Compare commits
16 Commits
Author | SHA1 | Date | |
---|---|---|---|
5e070e1875 | |||
dd780ad29d | |||
d2165d23bf | |||
d9d0f129b5 | |||
c8d56e62a2 | |||
67bd6ad36f | |||
1a924058a1 | |||
8f81b4d795 | |||
9edd3a53ce | |||
91a6164663 | |||
5a7e052ac4 | |||
6ba3617e32 | |||
c32c91829b | |||
b021935b10 | |||
3d99bf35fd | |||
c5f8ace5d2 |
5
.github/workflows/pr_checks.yml
vendored
5
.github/workflows/pr_checks.yml
vendored
@ -37,3 +37,8 @@ jobs:
|
||||
run: ./gradlew assemble
|
||||
- name: Run Unit Tests
|
||||
run: ./gradlew test
|
||||
- name: Cache Coverage Results
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ./**/build/reports/jacoco
|
||||
key: ${{ runner.os }}-unit-${{ env.GITHUB_SHA }}
|
||||
|
20
.github/workflows/publish.yml
vendored
20
.github/workflows/publish.yml
vendored
@ -24,3 +24,23 @@ jobs:
|
||||
run: ./gradlew publishAllPublicationsToGithubPackagesRepository
|
||||
env:
|
||||
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 }}
|
||||
|
70
CHANGELOG.md
70
CHANGELOG.md
@ -1,5 +1,75 @@
|
||||
# Changelog
|
||||
|
||||
## [1.11.0] - November 25th, 2021
|
||||
### Added
|
||||
- Support for Ktor Location Plugin
|
||||
|
||||
## [1.10.0] - November 25th, 2021
|
||||
|
||||
### Changed
|
||||
|
||||
- Added `operationId` parameter to `MethodInfo`
|
||||
|
||||
## [1.9.2] - October 24th, 2021
|
||||
|
||||
### Changed
|
||||
|
||||
- Jackson ObjectMapper passed by parameter to openapi module
|
||||
- Added serializable annotation to ExceptionResponse
|
||||
|
||||
## [1.9.1] - October 17th, 2021
|
||||
|
||||
### Changed
|
||||
|
||||
- Code Coverage removed from PR checks due to limitations with GitHub workflows
|
||||
- Minor linting fixes
|
||||
- Detekt now builds off of default config
|
||||
|
||||
## [1.9.0] - october 15th, 2021
|
||||
|
||||
### Added
|
||||
|
||||
- ByteArray added to the set of default types
|
||||
|
||||
|
||||
## [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
|
||||
|
||||
- Ability to add custom type schema overrides for edge case types.
|
||||
|
||||
## [1.5.1] - August 12th, 2021
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed bug where polymorphic types were not being rendered correctly when part of collections and maps
|
||||
|
||||
## [1.5.0] - July 25th, 2021
|
||||
|
||||
### Changed
|
||||
|
||||
- Added support for BigInteger and BigDecimal in response types
|
||||
|
||||
## [1.4.0] - July 22nd, 2021
|
||||
|
||||
### Changed
|
||||
|
229
README.md
229
README.md
@ -1,17 +1,21 @@
|
||||
# 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)
|
||||
|
||||
## What is Kompendium
|
||||
|
||||
Kompendium is intended to be a minimally invasive OpenApi Specification generator for [Ktor](https://ktor.io).
|
||||
Minimally invasive meaning that users will use only Ktor native functions when implementing their API, and will
|
||||
supplement with Kompendium code in order to generate the appropriate spec.
|
||||
### ⚠️ For info on V2 please see [here](#V2)
|
||||
|
||||
Kompendium is intended to be a minimally invasive OpenApi Specification generator for [Ktor](https://ktor.io). Minimally
|
||||
invasive meaning that users will use only Ktor native functions when implementing their API, and will supplement with
|
||||
Kompendium code in order to generate the appropriate spec.
|
||||
|
||||
## How to install
|
||||
|
||||
Kompendium publishes all releases to Maven Central. As such, using the stable version of `Kompendium` is as simple
|
||||
as declaring it as an implementation dependency in your `build.gradle.kts`
|
||||
Kompendium publishes all releases to Maven Central. As such, using the stable version of `Kompendium` is as simple as
|
||||
declaring it as an implementation dependency in your `build.gradle.kts`
|
||||
|
||||
```kotlin
|
||||
repositories {
|
||||
@ -19,17 +23,18 @@ repositories {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// other (less cool) dependencies
|
||||
implementation("io.bkbn:kompendium-core:latest")
|
||||
implementation("io.bkbn:kompendium-auth:latest")
|
||||
implementation("io.bkbn:kompendium-swagger-ui:latest")
|
||||
implementation("io.bkbn:kompendium-core:1.8.1")
|
||||
implementation("io.bkbn:kompendium-auth:1.8.1")
|
||||
implementation("io.bkbn:kompendium-swagger-ui:1.8.1")
|
||||
|
||||
// Other dependencies...
|
||||
}
|
||||
```
|
||||
|
||||
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
|
||||
from GitHub is slightly more involved, but such is the price you pay for bleeding edge fake data generation.
|
||||
If you want to get a little spicy 🤠 every merge of Kompendium is published to the GitHub package registry. Pulling from
|
||||
GitHub is slightly more involved, but such is the price you pay for bleeding edge fake data generation.
|
||||
|
||||
```kotlin
|
||||
// 1 Setup a helper function to import any Github Repository Package
|
||||
@ -41,27 +46,33 @@ fun RepositoryHandler.github(packageUrl: String) = maven {
|
||||
username = java.lang.System.getenv("GITHUB_USER")
|
||||
password = java.lang.System.getenv("GITHUB_TOKEN")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2 Add the repo in question (in this case Kompendium)
|
||||
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
|
||||
dependencies {
|
||||
implementation("io.bkbn:kompendium-core:1.0.0")
|
||||
implementation("io.bkbn:kompendium-core:1.8.1")
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## 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!
|
||||
|
||||
## In depth
|
||||
|
||||
### Notarized Routes
|
||||
|
||||
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
|
||||
a lot of the class based reflection that powers Kompendium. Generally speaking the three types that a `notarized` method
|
||||
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 a
|
||||
lot of the class based reflection that powers Kompendium. Generally speaking the three types that a `notarized` method
|
||||
will consume are
|
||||
|
||||
- `TParam`: Used to notarize expected request parameters
|
||||
@ -70,16 +81,17 @@ will consume are
|
||||
|
||||
`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 the `StatusPage`
|
||||
In addition to standard HTTP Methods, Kompendium also introduced the concept of `notarizedExceptions`. Using
|
||||
the `StatusPage`
|
||||
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
|
||||
|
||||
- `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.
|
||||
|
||||
In keeping with minimal invasion, these extension methods all consume the same code block as a standard Ktor route method,
|
||||
meaning that swapping in a default Ktor route and a Kompendium `notarized` route is as simple as a single method change.
|
||||
In keeping with minimal invasion, these extension methods all consume the same code block as a standard Ktor route
|
||||
method, meaning that swapping in a default Ktor route and a Kompendium `notarized` route is as simple as a single method
|
||||
change.
|
||||
|
||||
### Supplemental Annotations
|
||||
|
||||
@ -91,21 +103,115 @@ Currently, the annotations used by Kompendium are as follows
|
||||
- `KompendiumField`
|
||||
- `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
|
||||
(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
|
||||
|
||||
Out of the box, Kompendium has support for sealed classes. 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
|
||||
suggestions on better implementations are welcome 🤠
|
||||
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 suggestions on better implementations are welcome 🤠
|
||||
|
||||
### Serialization
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
Added the possibility to add your own ObjectMapper for Jackson.
|
||||
|
||||
Added a default parameter with the following configuration:
|
||||
|
||||
```kotlin
|
||||
ObjectMapper()
|
||||
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
|
||||
.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:
|
||||
|
||||
```kotlin
|
||||
routing {
|
||||
openApi(oas, objectMapper)
|
||||
route("/potato/spud") {
|
||||
notarizedGet(simpleGetInfo) {
|
||||
call.respond(HttpStatusCode.OK)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 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)
|
||||
|
||||
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 handler signature is as follows
|
||||
|
||||
```kotlin
|
||||
fun <T : RouteSelector> addCustomRouteHandler(
|
||||
selector: KClass<T>,
|
||||
handler: PathCalculator.(Route, String) -> String
|
||||
)
|
||||
```
|
||||
|
||||
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`, 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).
|
||||
|
||||
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
|
||||
|
||||
@ -139,6 +245,7 @@ fun Application.mainModule() {
|
||||
val simpleGetInfo = GetInfo<Unit, ExampleResponse>(
|
||||
summary = "Example Parameters",
|
||||
description = "A test for setting parameter examples",
|
||||
operationId = "getExamples",
|
||||
responseInfo = ResponseInfo(
|
||||
status = 200,
|
||||
description = "nice",
|
||||
@ -150,12 +257,13 @@ val simpleGetInfo = GetInfo<Unit, ExampleResponse>(
|
||||
|
||||
### Kompendium Auth and security schemes
|
||||
|
||||
There is a separate library to handle security schemes: `kompendium-auth`.
|
||||
This needs to be added to your project as dependency.
|
||||
There is a separate library to handle security schemes: `kompendium-auth`. This needs to be added to your project as
|
||||
dependency.
|
||||
|
||||
At the moment, the basic and jwt authentication is only supported.
|
||||
|
||||
A minimal example would be:
|
||||
|
||||
```kotlin
|
||||
install(Authentication) {
|
||||
notarizedBasic("basic") {
|
||||
@ -194,19 +302,23 @@ val jwtAuthGetInfo = basicAuthGetInfo.copy(securitySchemes = setOf("jwt"))
|
||||
```
|
||||
|
||||
### 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:
|
||||
|
||||
```kotlin
|
||||
install(Webjars)
|
||||
routing {
|
||||
openApi(oas)
|
||||
swaggerUI()
|
||||
}
|
||||
routing {
|
||||
openApi(oas)
|
||||
swaggerUI()
|
||||
}
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
```kotlin
|
||||
@ -216,25 +328,50 @@ routing {
|
||||
}
|
||||
```
|
||||
|
||||
## Custom Type Overrides
|
||||
|
||||
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. Should you encounter a
|
||||
data type that Kompendium cannot comprehend, you will need to add it explicitly. For example, adding the Joda
|
||||
Time `DateTime` object would be as simple as the following
|
||||
|
||||
```kotlin
|
||||
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
|
||||
type override can be cached ahead of reflection. Kompendium will then match all instances of this type and return the
|
||||
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.
|
||||
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.
|
||||
|
||||
## Limitations
|
||||
|
||||
### Kompendium as a singleton
|
||||
|
||||
Currently, Kompendium exists as a Kotlin object. This comes with a couple perks, but a couple downsides. Primarily,
|
||||
it offers a seriously clean UX where the implementer doesn't need to worry about what instance to send data to. The main
|
||||
Currently, Kompendium exists as a Kotlin object. This comes with a couple perks, but a couple downsides. Primarily, it
|
||||
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.
|
||||
|
||||
If this is a blocker, please open a GitHub issue, and we can start to think out solutions!
|
||||
|
||||
## 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
|
||||
should have. There are several outstanding features that have been added to the
|
||||
[V2 Milestone](https://github.com/bkbnio/kompendium/milestone/2). Among others, this includes
|
||||
|
||||
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 should have.
|
||||
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
|
||||
- Field Validation
|
||||
- MavenCentral Release
|
||||
|
||||
If you have a feature that you would like to see implemented that is not on this list, or discover a 🐞, please open
|
||||
an issue [here](https://github.com/bkbnio/kompendium/issues/new)
|
||||
If you have a feature that you would like to see implemented that is not on this list, or discover a 🐞, please open an
|
||||
issue [here](https://github.com/bkbnio/kompendium/issues/new)
|
||||
|
||||
### V2
|
||||
|
||||
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!
|
||||
If you are unsure where your changes should be, please open an issue first :)
|
||||
|
@ -4,8 +4,8 @@ import io.gitlab.arturbosch.detekt.extensions.DetektExtension
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
plugins {
|
||||
id("org.jetbrains.kotlin.jvm") version "1.5.0" apply false
|
||||
id("io.gitlab.arturbosch.detekt") version "1.17.0-RC3" apply false
|
||||
id("org.jetbrains.kotlin.jvm") version "1.5.31" apply false
|
||||
id("io.gitlab.arturbosch.detekt") version "1.18.1" 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
|
||||
}
|
||||
@ -30,6 +30,7 @@ allprojects {
|
||||
apply(plugin = "io.gitlab.arturbosch.detekt")
|
||||
apply(plugin = "com.adarshr.test-logger")
|
||||
apply(plugin = "idea")
|
||||
apply(plugin = "jacoco")
|
||||
|
||||
tasks.withType<KotlinCompile>().configureEach {
|
||||
kotlinOptions {
|
||||
@ -37,6 +38,22 @@ 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"
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
configure<TestLoggerExtension> {
|
||||
theme = ThemeType.MOCHA
|
||||
setLogLevel("lifecycle")
|
||||
@ -57,7 +74,7 @@ allprojects {
|
||||
}
|
||||
|
||||
configure<DetektExtension> {
|
||||
toolVersion = "1.17.0-RC3"
|
||||
toolVersion = "1.18.1"
|
||||
config = files("${rootProject.projectDir}/detekt.yml")
|
||||
buildUponDefaultConfig = true
|
||||
}
|
||||
|
702
detekt.yml
702
detekt.yml
@ -1,710 +1,20 @@
|
||||
build:
|
||||
maxIssues: 0
|
||||
excludeCorrectable: false
|
||||
weights:
|
||||
# complexity: 2
|
||||
# LongParameterList: 1
|
||||
# style: 1
|
||||
# comments: 1
|
||||
|
||||
config:
|
||||
validation: true
|
||||
warningsAsErrors: false
|
||||
# when writing own rules with new properties, exclude the property path e.g.: 'my_rule_set,.*>.*>[my_property]'
|
||||
excludes: ''
|
||||
|
||||
processors:
|
||||
active: true
|
||||
exclude:
|
||||
- 'DetektProgressListener'
|
||||
|
||||
console-reports:
|
||||
active: true
|
||||
exclude:
|
||||
- 'ProjectStatisticsReport'
|
||||
- 'ComplexityReport'
|
||||
- 'NotificationReport'
|
||||
- 'FileBasedFindingsReport'
|
||||
|
||||
output-reports:
|
||||
active: true
|
||||
|
||||
comments:
|
||||
active: true
|
||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
AbsentOrWrongFileLicense:
|
||||
active: false
|
||||
licenseTemplateFile: 'license.template'
|
||||
CommentOverPrivateFunction:
|
||||
active: false
|
||||
CommentOverPrivateProperty:
|
||||
active: false
|
||||
EndOfSentenceFormat:
|
||||
active: false
|
||||
endOfSentenceFormat: '([.?!][ \t\n\r\f<])|([.?!:]$)'
|
||||
UndocumentedPublicClass:
|
||||
active: false
|
||||
searchInNestedClass: true
|
||||
searchInInnerClass: true
|
||||
searchInInnerObject: true
|
||||
searchInInnerInterface: true
|
||||
UndocumentedPublicFunction:
|
||||
active: false
|
||||
UndocumentedPublicProperty:
|
||||
active: false
|
||||
|
||||
complexity:
|
||||
active: true
|
||||
ComplexCondition:
|
||||
active: true
|
||||
threshold: 4
|
||||
ComplexInterface:
|
||||
TooManyFunctions:
|
||||
active: false
|
||||
threshold: 10
|
||||
includeStaticDeclarations: false
|
||||
includePrivateDeclarations: false
|
||||
ComplexMethod:
|
||||
active: true
|
||||
threshold: 25
|
||||
ignoreSingleWhenExpression: false
|
||||
ignoreSimpleWhenEntries: false
|
||||
ignoreNestingFunctions: false
|
||||
nestingFunctions: [run, let, apply, with, also, use, forEach, isNotNull, ifNull]
|
||||
LabeledExpression:
|
||||
active: false
|
||||
ignoredLabels: []
|
||||
LargeClass:
|
||||
active: true
|
||||
threshold: 600
|
||||
LongMethod:
|
||||
active: true
|
||||
threshold: 80
|
||||
LongParameterList:
|
||||
active: true
|
||||
functionThreshold: 10
|
||||
constructorThreshold: 10
|
||||
ignoreDefaultParameters: false
|
||||
ignoreDataClasses: true
|
||||
ignoreAnnotated: []
|
||||
MethodOverloading:
|
||||
active: false
|
||||
threshold: 6
|
||||
NamedArguments:
|
||||
active: false
|
||||
threshold: 3
|
||||
NestedBlockDepth:
|
||||
active: true
|
||||
threshold: 6
|
||||
ReplaceSafeCallChainWithRun:
|
||||
active: false
|
||||
StringLiteralDuplication:
|
||||
active: false
|
||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
threshold: 3
|
||||
ignoreAnnotation: true
|
||||
excludeStringsWithLessThan5Characters: true
|
||||
ignoreStringsRegex: '$^'
|
||||
TooManyFunctions:
|
||||
active: false
|
||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
thresholdInFiles: 11
|
||||
thresholdInClasses: 11
|
||||
thresholdInInterfaces: 11
|
||||
thresholdInObjects: 11
|
||||
thresholdInEnums: 11
|
||||
ignoreDeprecated: false
|
||||
ignorePrivate: false
|
||||
ignoreOverridden: false
|
||||
|
||||
coroutines:
|
||||
active: true
|
||||
GlobalCoroutineUsage:
|
||||
active: false
|
||||
RedundantSuspendModifier:
|
||||
active: false
|
||||
SleepInsteadOfDelay:
|
||||
active: false
|
||||
SuspendFunWithFlowReturnType:
|
||||
active: false
|
||||
|
||||
empty-blocks:
|
||||
active: true
|
||||
EmptyCatchBlock:
|
||||
active: true
|
||||
allowedExceptionNameRegex: '_|(ignore|expected).*'
|
||||
EmptyClassBlock:
|
||||
active: true
|
||||
EmptyDefaultConstructor:
|
||||
active: true
|
||||
EmptyDoWhileBlock:
|
||||
active: true
|
||||
EmptyElseBlock:
|
||||
active: true
|
||||
EmptyFinallyBlock:
|
||||
active: true
|
||||
EmptyForBlock:
|
||||
active: true
|
||||
EmptyFunctionBlock:
|
||||
active: true
|
||||
ignoreOverridden: false
|
||||
EmptyIfBlock:
|
||||
active: true
|
||||
EmptyInitBlock:
|
||||
active: true
|
||||
EmptyKtFile:
|
||||
active: true
|
||||
EmptySecondaryConstructor:
|
||||
active: true
|
||||
EmptyTryBlock:
|
||||
active: true
|
||||
EmptyWhenBlock:
|
||||
active: true
|
||||
EmptyWhileBlock:
|
||||
active: true
|
||||
|
||||
exceptions:
|
||||
active: true
|
||||
ExceptionRaisedInUnexpectedLocation:
|
||||
active: true
|
||||
methodNames: [toString, hashCode, equals, finalize]
|
||||
InstanceOfCheckForException:
|
||||
active: false
|
||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
NotImplementedDeclaration:
|
||||
active: false
|
||||
ObjectExtendsThrowable:
|
||||
active: false
|
||||
PrintStackTrace:
|
||||
active: true
|
||||
RethrowCaughtException:
|
||||
active: true
|
||||
ReturnFromFinally:
|
||||
active: true
|
||||
ignoreLabeled: false
|
||||
SwallowedException:
|
||||
active: true
|
||||
ignoredExceptionTypes:
|
||||
- InterruptedException
|
||||
- NumberFormatException
|
||||
- ParseException
|
||||
- MalformedURLException
|
||||
allowedExceptionNameRegex: '_|(ignore|expected).*'
|
||||
ThrowingExceptionFromFinally:
|
||||
active: true
|
||||
ThrowingExceptionInMain:
|
||||
active: false
|
||||
ThrowingExceptionsWithoutMessageOrCause:
|
||||
active: true
|
||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
exceptions:
|
||||
- IllegalArgumentException
|
||||
- IllegalStateException
|
||||
- IOException
|
||||
ThrowingNewInstanceOfSameException:
|
||||
active: true
|
||||
TooGenericExceptionCaught:
|
||||
active: true
|
||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
exceptionNames:
|
||||
- ArrayIndexOutOfBoundsException
|
||||
- Error
|
||||
- Exception
|
||||
- IllegalMonitorStateException
|
||||
- NullPointerException
|
||||
- IndexOutOfBoundsException
|
||||
- RuntimeException
|
||||
- Throwable
|
||||
allowedExceptionNameRegex: '_|(ignore|expected).*'
|
||||
TooGenericExceptionThrown:
|
||||
active: true
|
||||
exceptionNames:
|
||||
- Error
|
||||
- Exception
|
||||
- Throwable
|
||||
- RuntimeException
|
||||
|
||||
ComplexMethod:
|
||||
threshold: 20
|
||||
formatting:
|
||||
active: true
|
||||
android: false
|
||||
autoCorrect: true
|
||||
AnnotationOnSeparateLine:
|
||||
active: false
|
||||
autoCorrect: true
|
||||
AnnotationSpacing:
|
||||
active: false
|
||||
autoCorrect: true
|
||||
ArgumentListWrapping:
|
||||
active: false
|
||||
autoCorrect: true
|
||||
ChainWrapping:
|
||||
active: true
|
||||
autoCorrect: true
|
||||
CommentSpacing:
|
||||
active: true
|
||||
autoCorrect: true
|
||||
EnumEntryNameCase:
|
||||
active: false
|
||||
autoCorrect: true
|
||||
Filename:
|
||||
active: true
|
||||
FinalNewline:
|
||||
active: true
|
||||
autoCorrect: true
|
||||
insertFinalNewLine: true
|
||||
ImportOrdering:
|
||||
active: false
|
||||
autoCorrect: true
|
||||
layout: 'idea'
|
||||
Indentation:
|
||||
active: false
|
||||
autoCorrect: true
|
||||
indentSize: 4
|
||||
continuationIndentSize: 4
|
||||
MaximumLineLength:
|
||||
active: true
|
||||
maxLineLength: 120
|
||||
ModifierOrdering:
|
||||
active: true
|
||||
autoCorrect: true
|
||||
MultiLineIfElse:
|
||||
active: true
|
||||
autoCorrect: true
|
||||
NoBlankLineBeforeRbrace:
|
||||
active: true
|
||||
autoCorrect: true
|
||||
NoConsecutiveBlankLines:
|
||||
active: true
|
||||
autoCorrect: true
|
||||
NoEmptyClassBody:
|
||||
active: true
|
||||
autoCorrect: true
|
||||
NoEmptyFirstLineInMethodBlock:
|
||||
active: false
|
||||
autoCorrect: true
|
||||
NoLineBreakAfterElse:
|
||||
active: true
|
||||
autoCorrect: true
|
||||
NoLineBreakBeforeAssignment:
|
||||
active: true
|
||||
autoCorrect: true
|
||||
NoMultipleSpaces:
|
||||
active: true
|
||||
autoCorrect: true
|
||||
NoSemicolons:
|
||||
active: true
|
||||
autoCorrect: true
|
||||
NoTrailingSpaces:
|
||||
active: true
|
||||
autoCorrect: true
|
||||
NoUnitReturn:
|
||||
active: true
|
||||
autoCorrect: true
|
||||
NoUnusedImports:
|
||||
active: true
|
||||
autoCorrect: true
|
||||
NoWildcardImports:
|
||||
active: true
|
||||
PackageName:
|
||||
active: true
|
||||
autoCorrect: true
|
||||
ParameterListWrapping:
|
||||
active: false
|
||||
autoCorrect: true
|
||||
indentSize: 4
|
||||
SpacingAroundAngleBrackets:
|
||||
active: false
|
||||
autoCorrect: true
|
||||
SpacingAroundColon:
|
||||
active: true
|
||||
autoCorrect: true
|
||||
SpacingAroundComma:
|
||||
active: true
|
||||
autoCorrect: true
|
||||
SpacingAroundCurly:
|
||||
active: true
|
||||
autoCorrect: true
|
||||
SpacingAroundDot:
|
||||
active: true
|
||||
autoCorrect: true
|
||||
SpacingAroundDoubleColon:
|
||||
active: false
|
||||
autoCorrect: true
|
||||
SpacingAroundKeyword:
|
||||
active: true
|
||||
autoCorrect: true
|
||||
SpacingAroundOperators:
|
||||
active: true
|
||||
autoCorrect: true
|
||||
SpacingAroundParens:
|
||||
active: true
|
||||
autoCorrect: true
|
||||
SpacingAroundRangeOperator:
|
||||
active: true
|
||||
autoCorrect: true
|
||||
SpacingAroundUnaryOperator:
|
||||
active: false
|
||||
autoCorrect: true
|
||||
SpacingBetweenDeclarationsWithAnnotations:
|
||||
active: false
|
||||
autoCorrect: true
|
||||
SpacingBetweenDeclarationsWithComments:
|
||||
active: false
|
||||
autoCorrect: true
|
||||
StringTemplate:
|
||||
active: true
|
||||
autoCorrect: true
|
||||
|
||||
naming:
|
||||
active: true
|
||||
ClassNaming:
|
||||
active: true
|
||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
classPattern: '[A-Z][a-zA-Z0-9]*'
|
||||
ConstructorParameterNaming:
|
||||
active: false
|
||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
parameterPattern: '[a-z][A-Za-z0-9]*'
|
||||
privateParameterPattern: '[a-z][A-Za-z0-9]*'
|
||||
excludeClassPattern: '$^'
|
||||
ignoreOverridden: true
|
||||
EnumNaming:
|
||||
active: true
|
||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
enumEntryPattern: '[A-Z][_a-zA-Z0-9]*'
|
||||
ForbiddenClassName:
|
||||
active: false
|
||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
forbiddenName: []
|
||||
FunctionMaxLength:
|
||||
active: false
|
||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
maximumFunctionNameLength: 30
|
||||
FunctionMinLength:
|
||||
active: false
|
||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
minimumFunctionNameLength: 3
|
||||
FunctionNaming:
|
||||
active: true
|
||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
functionPattern: '([a-z][a-zA-Z0-9]*)|(`.*`)'
|
||||
excludeClassPattern: '$^'
|
||||
ignoreOverridden: true
|
||||
ignoreAnnotated: ['Composable']
|
||||
FunctionParameterNaming:
|
||||
active: true
|
||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
parameterPattern: '[a-z][A-Za-z0-9]*'
|
||||
excludeClassPattern: '$^'
|
||||
ignoreOverridden: true
|
||||
InvalidPackageDeclaration:
|
||||
active: false
|
||||
excludes: ['*.kts']
|
||||
rootPackage: ''
|
||||
MatchingDeclarationName:
|
||||
active: true
|
||||
mustBeFirst: true
|
||||
MemberNameEqualsClassName:
|
||||
active: true
|
||||
ignoreOverridden: true
|
||||
NonBooleanPropertyPrefixedWithIs:
|
||||
active: false
|
||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
ObjectPropertyNaming:
|
||||
active: true
|
||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
constantPattern: '[A-Za-z][_A-Za-z0-9]*'
|
||||
propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
|
||||
privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*'
|
||||
PackageNaming:
|
||||
active: true
|
||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*'
|
||||
TopLevelPropertyNaming:
|
||||
active: true
|
||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
constantPattern: '[A-Z][_A-Z0-9]*'
|
||||
propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
|
||||
privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*'
|
||||
VariableMaxLength:
|
||||
active: false
|
||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
maximumVariableNameLength: 64
|
||||
VariableMinLength:
|
||||
active: false
|
||||
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
minimumVariableNameLength: 1
|
||||
VariableNaming:
|
||||
active: true
|
||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
variablePattern: '[a-z][A-Za-z0-9]*'
|
||||
privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*'
|
||||
excludeClassPattern: '$^'
|
||||
ignoreOverridden: true
|
||||
|
||||
performance:
|
||||
active: true
|
||||
ArrayPrimitive:
|
||||
active: true
|
||||
ForEachOnRange:
|
||||
active: true
|
||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
SpreadOperator:
|
||||
active: true
|
||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
UnnecessaryTemporaryInstantiation:
|
||||
active: true
|
||||
|
||||
potential-bugs:
|
||||
active: true
|
||||
Deprecation:
|
||||
active: false
|
||||
DontDowncastCollectionTypes:
|
||||
active: false
|
||||
DuplicateCaseInWhenExpression:
|
||||
active: true
|
||||
EqualsAlwaysReturnsTrueOrFalse:
|
||||
active: true
|
||||
EqualsWithHashCodeExist:
|
||||
active: true
|
||||
ExitOutsideMain:
|
||||
active: false
|
||||
ExplicitGarbageCollectionCall:
|
||||
active: true
|
||||
HasPlatformType:
|
||||
active: false
|
||||
IgnoredReturnValue:
|
||||
active: false
|
||||
restrictToAnnotatedMethods: true
|
||||
returnValueAnnotations: ['*.CheckReturnValue', '*.CheckResult']
|
||||
ImplicitDefaultLocale:
|
||||
active: true
|
||||
ImplicitUnitReturnType:
|
||||
active: false
|
||||
allowExplicitReturnType: true
|
||||
InvalidRange:
|
||||
active: true
|
||||
IteratorHasNextCallsNextMethod:
|
||||
active: true
|
||||
IteratorNotThrowingNoSuchElementException:
|
||||
active: true
|
||||
LateinitUsage:
|
||||
active: false
|
||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
excludeAnnotatedProperties: []
|
||||
ignoreOnClassesPattern: ''
|
||||
MapGetWithNotNullAssertionOperator:
|
||||
active: false
|
||||
MissingWhenCase:
|
||||
active: true
|
||||
allowElseExpression: true
|
||||
NullableToStringCall:
|
||||
active: false
|
||||
RedundantElseInWhen:
|
||||
active: true
|
||||
UnconditionalJumpStatementInLoop:
|
||||
active: false
|
||||
UnnecessaryNotNullOperator:
|
||||
active: true
|
||||
UnnecessarySafeCall:
|
||||
active: true
|
||||
UnreachableCatchBlock:
|
||||
active: false
|
||||
UnreachableCode:
|
||||
active: true
|
||||
UnsafeCallOnNullableType:
|
||||
active: true
|
||||
UnsafeCast:
|
||||
active: true
|
||||
UselessPostfixExpression:
|
||||
active: false
|
||||
WrongEqualsTypeParameter:
|
||||
active: true
|
||||
|
||||
style:
|
||||
active: true
|
||||
ClassOrdering:
|
||||
active: false
|
||||
CollapsibleIfStatements:
|
||||
active: false
|
||||
DataClassContainsFunctions:
|
||||
active: false
|
||||
conversionFunctionPrefix: 'to'
|
||||
DataClassShouldBeImmutable:
|
||||
active: false
|
||||
DestructuringDeclarationWithTooManyEntries:
|
||||
active: false
|
||||
maxDestructuringEntries: 3
|
||||
EqualsNullCall:
|
||||
active: true
|
||||
EqualsOnSignatureLine:
|
||||
active: false
|
||||
ExplicitCollectionElementAccessMethod:
|
||||
active: false
|
||||
ExplicitItLambdaParameter:
|
||||
active: false
|
||||
ExpressionBodySyntax:
|
||||
active: false
|
||||
includeLineWrapping: false
|
||||
ForbiddenComment:
|
||||
active: true
|
||||
values: ['TODO:', 'FIXME:', 'STOPSHIP:']
|
||||
allowedPatterns: ''
|
||||
ForbiddenImport:
|
||||
active: false
|
||||
imports: []
|
||||
forbiddenPatterns: ''
|
||||
ForbiddenMethodCall:
|
||||
active: false
|
||||
methods: ['kotlin.io.println', 'kotlin.io.print']
|
||||
ForbiddenPublicDataClass:
|
||||
active: true
|
||||
excludes: ['**']
|
||||
ignorePackages: ['*.internal', '*.internal.*']
|
||||
ForbiddenVoid:
|
||||
active: false
|
||||
ignoreOverridden: false
|
||||
ignoreUsageInGenerics: false
|
||||
FunctionOnlyReturningConstant:
|
||||
active: true
|
||||
ignoreOverridableFunction: true
|
||||
ignoreActualFunction: true
|
||||
excludedFunctions: 'describeContents'
|
||||
excludeAnnotatedFunction: ['dagger.Provides']
|
||||
LibraryCodeMustSpecifyReturnType:
|
||||
active: true
|
||||
excludes: ['**']
|
||||
LibraryEntitiesShouldNotBePublic:
|
||||
active: true
|
||||
excludes: ['**']
|
||||
LoopWithTooManyJumpStatements:
|
||||
active: true
|
||||
maxJumpCount: 1
|
||||
MagicNumber:
|
||||
active: true
|
||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
ignoreNumbers: ['-1', '0', '1', '2']
|
||||
ignoreHashCodeFunction: true
|
||||
ignorePropertyDeclaration: false
|
||||
ignoreLocalVariableDeclaration: false
|
||||
ignoreConstantDeclaration: true
|
||||
ignoreCompanionObjectPropertyDeclaration: true
|
||||
ignoreAnnotation: false
|
||||
ignoreNamedArgument: true
|
||||
ignoreEnums: false
|
||||
ignoreRanges: false
|
||||
ignoreExtensionFunctions: true
|
||||
MandatoryBracesIfStatements:
|
||||
active: false
|
||||
MandatoryBracesLoops:
|
||||
active: false
|
||||
MaxLineLength:
|
||||
excludes: ['**/test/**/*', '**/testIntegration/**/*']
|
||||
active: true
|
||||
maxLineLength: 120
|
||||
excludePackageStatements: true
|
||||
excludeImportStatements: true
|
||||
excludeCommentStatements: false
|
||||
MayBeConst:
|
||||
active: true
|
||||
ModifierOrder:
|
||||
active: true
|
||||
MultilineLambdaItParameter:
|
||||
naming:
|
||||
ConstructorParameterNaming:
|
||||
active: false
|
||||
NestedClassesVisibility:
|
||||
active: true
|
||||
NewLineAtEndOfFile:
|
||||
active: true
|
||||
NoTabs:
|
||||
active: false
|
||||
OptionalAbstractKeyword:
|
||||
active: true
|
||||
OptionalUnit:
|
||||
active: false
|
||||
OptionalWhenBraces:
|
||||
active: false
|
||||
PreferToOverPairSyntax:
|
||||
active: false
|
||||
ProtectedMemberInFinalClass:
|
||||
active: true
|
||||
RedundantExplicitType:
|
||||
active: false
|
||||
RedundantHigherOrderMapUsage:
|
||||
active: false
|
||||
RedundantVisibilityModifierRule:
|
||||
active: false
|
||||
ReturnCount:
|
||||
active: true
|
||||
max: 2
|
||||
excludedFunctions: 'equals'
|
||||
excludeLabeled: false
|
||||
excludeReturnFromLambda: true
|
||||
excludeGuardClauses: false
|
||||
SafeCast:
|
||||
active: true
|
||||
SerialVersionUIDInSerializableClass:
|
||||
active: true
|
||||
SpacingBetweenPackageAndImports:
|
||||
active: false
|
||||
ThrowsCount:
|
||||
active: true
|
||||
max: 2
|
||||
TrailingWhitespace:
|
||||
active: false
|
||||
UnderscoresInNumericLiterals:
|
||||
active: false
|
||||
acceptableDecimalLength: 5
|
||||
UnnecessaryAbstractClass:
|
||||
active: true
|
||||
excludeAnnotatedClasses: ['dagger.Module']
|
||||
UnnecessaryAnnotationUseSiteTarget:
|
||||
active: false
|
||||
UnnecessaryApply:
|
||||
active: true
|
||||
UnnecessaryFilter:
|
||||
active: false
|
||||
UnnecessaryInheritance:
|
||||
active: true
|
||||
UnnecessaryLet:
|
||||
active: false
|
||||
UnnecessaryParentheses:
|
||||
active: false
|
||||
UntilInsteadOfRangeTo:
|
||||
active: false
|
||||
UnusedImports:
|
||||
active: false
|
||||
UnusedPrivateClass:
|
||||
active: true
|
||||
UnusedPrivateMember:
|
||||
active: true
|
||||
allowedNames: '(_|ignored|expected|serialVersionUID)'
|
||||
UseArrayLiteralsInAnnotations:
|
||||
active: false
|
||||
UseCheckNotNull:
|
||||
active: false
|
||||
UseCheckOrError:
|
||||
active: false
|
||||
UseDataClass:
|
||||
active: false
|
||||
excludeAnnotatedClasses: []
|
||||
allowVars: false
|
||||
UseEmptyCounterpart:
|
||||
active: false
|
||||
UseIfEmptyOrIfBlank:
|
||||
active: false
|
||||
UseIfInsteadOfWhen:
|
||||
active: false
|
||||
UseIsNullOrEmpty:
|
||||
active: false
|
||||
UseRequire:
|
||||
active: false
|
||||
UseRequireNotNull:
|
||||
active: false
|
||||
UselessCallOnNotNull:
|
||||
active: true
|
||||
UtilityClassWithPublicConstructor:
|
||||
active: true
|
||||
VarCouldBeVal:
|
||||
active: true
|
||||
WildcardImport:
|
||||
active: true
|
||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
excludeImports: ['java.util.*', 'kotlinx.android.synthetic.*']
|
||||
|
@ -1,5 +1,5 @@
|
||||
# Kompendium
|
||||
project.version=1.4.0
|
||||
project.version=1.11.0
|
||||
# Kotlin
|
||||
kotlin.code.style=official
|
||||
# Gradle
|
||||
|
@ -1,6 +1,6 @@
|
||||
[versions]
|
||||
kotlin = "1.4.32"
|
||||
ktor = "1.5.3"
|
||||
ktor = "1.6.5"
|
||||
kotlinx-serialization = "1.2.1"
|
||||
jackson-kotlin = "2.12.0"
|
||||
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-jwt = { group = "io.ktor", name = "ktor-auth-jwt", 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
|
||||
jackson-module-kotlin = { group = "com.fasterxml.jackson.module", name = "jackson-module-kotlin", version.ref = "jackson-kotlin" }
|
||||
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
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
|
||||
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.bkbn.kompendium.Kompendium
|
||||
import io.bkbn.kompendium.models.oas.OpenApiSpecSchemaSecurity
|
||||
import io.ktor.auth.AuthenticationRouteSelector
|
||||
|
||||
object KompendiumAuth {
|
||||
|
||||
init {
|
||||
Kompendium.pathCalculator = AuthPathCalculator()
|
||||
Kompendium.addCustomRouteHandler(AuthenticationRouteSelector::class) { route, tail ->
|
||||
calculate(route.parent, tail)
|
||||
}
|
||||
}
|
||||
|
||||
fun Authentication.Configuration.notarizedBasic(
|
||||
|
@ -4,8 +4,12 @@ import io.bkbn.kompendium.models.meta.ErrorMap
|
||||
import io.bkbn.kompendium.models.meta.SchemaMap
|
||||
import io.bkbn.kompendium.models.oas.OpenApiSpec
|
||||
import io.bkbn.kompendium.models.oas.OpenApiSpecInfo
|
||||
import io.bkbn.kompendium.path.CorePathCalculator
|
||||
import io.bkbn.kompendium.models.oas.TypedSchema
|
||||
import io.bkbn.kompendium.path.IPathCalculator
|
||||
import io.bkbn.kompendium.path.PathCalculator
|
||||
import io.ktor.routing.Route
|
||||
import io.ktor.routing.RouteSelector
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
/**
|
||||
* Maintains all state for the Kompendium library
|
||||
@ -21,7 +25,7 @@ object Kompendium {
|
||||
paths = mutableMapOf()
|
||||
)
|
||||
|
||||
var pathCalculator: PathCalculator = CorePathCalculator()
|
||||
fun calculatePath(route: Route) = PathCalculator.calculate(route)
|
||||
|
||||
fun resetSchema() {
|
||||
openApiSpec = OpenApiSpec(
|
||||
@ -31,4 +35,15 @@ object Kompendium {
|
||||
)
|
||||
cache = emptyMap()
|
||||
}
|
||||
|
||||
fun addCustomTypeSchema(clazz: KClass<*>, schema: TypedSchema) {
|
||||
cache = cache.plus(clazz.simpleName!! to schema)
|
||||
}
|
||||
|
||||
fun <T : RouteSelector> addCustomRouteHandler(
|
||||
selector: KClass<T>,
|
||||
handler: IPathCalculator.(Route, String) -> String
|
||||
) {
|
||||
PathCalculator.addCustomRouteHandler(selector, handler)
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,7 @@
|
||||
package io.bkbn.kompendium
|
||||
|
||||
import io.ktor.routing.Route
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.KType
|
||||
import kotlin.reflect.full.createType
|
||||
import kotlin.reflect.typeOf
|
||||
|
||||
/**
|
||||
@ -49,23 +47,8 @@ object KompendiumPreFlight {
|
||||
}
|
||||
|
||||
fun addToCache(paramType: KType, requestType: KType, responseType: KType) {
|
||||
gatherSubTypes(requestType).forEach {
|
||||
Kompendium.cache = Kontent.generateKontent(it, Kompendium.cache)
|
||||
}
|
||||
gatherSubTypes(responseType).forEach {
|
||||
Kompendium.cache = Kontent.generateKontent(it, Kompendium.cache)
|
||||
}
|
||||
Kompendium.cache = Kontent.generateKontent(requestType, Kompendium.cache)
|
||||
Kompendium.cache = Kontent.generateKontent(responseType, Kompendium.cache)
|
||||
Kompendium.cache = Kontent.generateParameterKontent(paramType, Kompendium.cache)
|
||||
}
|
||||
|
||||
private fun gatherSubTypes(type: KType): List<KType> {
|
||||
val classifier = type.classifier as KClass<*>
|
||||
return if (classifier.isSealed) {
|
||||
classifier.sealedSubclasses.map {
|
||||
it.createType(type.arguments)
|
||||
}
|
||||
} else {
|
||||
listOf(type)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
package io.bkbn.kompendium
|
||||
|
||||
import io.bkbn.kompendium.annotations.UndeclaredField
|
||||
import io.bkbn.kompendium.models.meta.SchemaMap
|
||||
import io.bkbn.kompendium.models.oas.AnyOfReferencedSchema
|
||||
import io.bkbn.kompendium.models.oas.ArraySchema
|
||||
@ -14,6 +15,9 @@ import io.bkbn.kompendium.util.Helpers.genericNameAdapter
|
||||
import io.bkbn.kompendium.util.Helpers.getReferenceSlug
|
||||
import io.bkbn.kompendium.util.Helpers.getSimpleSlug
|
||||
import io.bkbn.kompendium.util.Helpers.logged
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.math.BigDecimal
|
||||
import java.math.BigInteger
|
||||
import java.util.UUID
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.KType
|
||||
@ -22,7 +26,6 @@ import kotlin.reflect.full.isSubclassOf
|
||||
import kotlin.reflect.full.memberProperties
|
||||
import kotlin.reflect.jvm.javaField
|
||||
import kotlin.reflect.typeOf
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
/**
|
||||
* Responsible for generating the schema map that is used to power all object references across the API Spec.
|
||||
@ -55,7 +58,22 @@ object Kontent {
|
||||
type: KType,
|
||||
cache: SchemaMap = emptyMap()
|
||||
): SchemaMap {
|
||||
return generateKTypeKontent(type, cache)
|
||||
var newCache = cache
|
||||
gatherSubTypes(type).forEach {
|
||||
newCache = generateKTypeKontent(it, newCache)
|
||||
}
|
||||
return newCache
|
||||
}
|
||||
|
||||
private fun gatherSubTypes(type: KType): List<KType> {
|
||||
val classifier = type.classifier as KClass<*>
|
||||
return if (classifier.isSealed) {
|
||||
classifier.sealedSubclasses.map {
|
||||
it.createType(type.arguments)
|
||||
}
|
||||
} else {
|
||||
listOf(type)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -106,6 +124,9 @@ object Kontent {
|
||||
String::class -> cache.plus(clazz.simpleName!! to SimpleSchema("string"))
|
||||
Boolean::class -> cache.plus(clazz.simpleName!! to SimpleSchema("boolean"))
|
||||
UUID::class -> cache.plus(clazz.simpleName!! to FormatSchema("uuid", "string"))
|
||||
BigDecimal::class -> cache.plus(clazz.simpleName!! to FormatSchema("double", "number"))
|
||||
BigInteger::class -> cache.plus(clazz.simpleName!! to FormatSchema("int64", "integer"))
|
||||
ByteArray::class -> cache.plus(clazz.simpleName!! to FormatSchema("byte", "string"))
|
||||
else -> when {
|
||||
clazz.isSubclassOf(Collection::class) -> handleCollectionType(type, clazz, cache)
|
||||
clazz.isSubclassOf(Enum::class) -> handleEnumType(clazz, cache)
|
||||
@ -120,6 +141,8 @@ object Kontent {
|
||||
* @param clazz Class of the object to analyze
|
||||
* @param cache Existing schema map to append to
|
||||
*/
|
||||
// TODO Fix as part of this issue https://github.com/bkbnio/kompendium/issues/80
|
||||
@Suppress("LongMethod", "ComplexMethod")
|
||||
private fun handleComplexType(type: KType, clazz: KClass<*>, cache: SchemaMap): SchemaMap {
|
||||
// This needs to be simple because it will be stored under it's appropriate reference component implicitly
|
||||
val slug = type.getSimpleSlug()
|
||||
@ -185,8 +208,14 @@ object Kontent {
|
||||
}
|
||||
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")
|
||||
val schema = ObjectSchema(fieldMap)
|
||||
val schema = ObjectSchema(fieldMap.plus(undeclaredFieldMap))
|
||||
logger.debug("$slug schema: $schema")
|
||||
newCache.plus(slug to schema)
|
||||
}
|
||||
@ -216,11 +245,20 @@ object Kontent {
|
||||
if (keyType?.classifier != String::class) {
|
||||
error("Invalid Map $type: OpenAPI dictionaries must have keys of type String")
|
||||
}
|
||||
val valClassName = (valType?.classifier as KClass<*>).simpleName
|
||||
val valClass = valType?.classifier as KClass<*>
|
||||
val valClassName = valClass.simpleName
|
||||
val referenceName = genericNameAdapter(type, clazz)
|
||||
val valueReference = ReferencedSchema("$COMPONENT_SLUG/$valClassName")
|
||||
val valueReference = when (valClass.isSealed) {
|
||||
true -> {
|
||||
val subTypes = gatherSubTypes(valType)
|
||||
AnyOfReferencedSchema(subTypes.map {
|
||||
ReferencedSchema(("$COMPONENT_SLUG/${it.getSimpleSlug()}"))
|
||||
})
|
||||
}
|
||||
false -> ReferencedSchema("$COMPONENT_SLUG/$valClassName")
|
||||
}
|
||||
val schema = DictionarySchema(additionalProperties = valueReference)
|
||||
val updatedCache = generateKTypeKontent(valType, cache)
|
||||
val updatedCache = generateKontent(valType, cache)
|
||||
return updatedCache.plus(referenceName to schema)
|
||||
}
|
||||
|
||||
@ -236,9 +274,17 @@ object Kontent {
|
||||
val collectionClass = collectionType.classifier as KClass<*>
|
||||
logger.debug("Obtained collection class: $collectionClass")
|
||||
val referenceName = genericNameAdapter(type, clazz)
|
||||
val valueReference = ReferencedSchema("${COMPONENT_SLUG}/${collectionClass.simpleName}")
|
||||
val valueReference = when (collectionClass.isSealed) {
|
||||
true -> {
|
||||
val subTypes = gatherSubTypes(collectionType)
|
||||
AnyOfReferencedSchema(subTypes.map {
|
||||
ReferencedSchema(("$COMPONENT_SLUG/${it.getSimpleSlug()}"))
|
||||
})
|
||||
}
|
||||
false -> ReferencedSchema("$COMPONENT_SLUG/${collectionClass.simpleName}")
|
||||
}
|
||||
val schema = ArraySchema(items = valueReference)
|
||||
val updatedCache = generateKTypeKontent(collectionType, cache)
|
||||
val updatedCache = generateKontent(collectionType, cache)
|
||||
return updatedCache.plus(referenceName to schema)
|
||||
}
|
||||
}
|
||||
|
@ -49,6 +49,7 @@ object MethodParser {
|
||||
) = OpenApiSpecPathItemOperation(
|
||||
summary = info.summary,
|
||||
description = info.description,
|
||||
operationId = info.operationId,
|
||||
tags = info.tags,
|
||||
deprecated = info.deprecated,
|
||||
parameters = paramType.toParameterSpec(),
|
||||
@ -173,7 +174,9 @@ object MethodParser {
|
||||
*/
|
||||
private fun KType.toParameterSpec(): List<OpenApiSpecParameter> {
|
||||
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
|
||||
?: error("Unable to parse field type from $prop")
|
||||
val anny = prop.findAnnotation<KompendiumParam>()
|
||||
|
@ -36,7 +36,7 @@ object Notarized {
|
||||
noinline body: PipelineInterceptor<Unit, ApplicationCall>
|
||||
): Route =
|
||||
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[path]?.get = parseMethodInfo(info, paramType, requestType, responseType)
|
||||
return method(HttpMethod.Get) { handle(body) }
|
||||
@ -55,7 +55,7 @@ object Notarized {
|
||||
noinline body: PipelineInterceptor<Unit, ApplicationCall>
|
||||
): Route =
|
||||
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[path]?.post = parseMethodInfo(info, paramType, requestType, responseType)
|
||||
return method(HttpMethod.Post) { handle(body) }
|
||||
@ -74,7 +74,7 @@ object Notarized {
|
||||
noinline body: PipelineInterceptor<Unit, ApplicationCall>,
|
||||
): Route =
|
||||
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[path]?.put =
|
||||
parseMethodInfo(info, paramType, requestType, responseType)
|
||||
@ -93,7 +93,7 @@ object Notarized {
|
||||
noinline body: PipelineInterceptor<Unit, ApplicationCall>
|
||||
): Route =
|
||||
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[path]?.delete = parseMethodInfo(info, paramType, requestType, responseType)
|
||||
return method(HttpMethod.Delete) { handle(body) }
|
||||
@ -112,5 +112,4 @@ object Notarized {
|
||||
info.parseErrorInfo(errorType, responseType)
|
||||
exception(handler)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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<*>)
|
@ -11,6 +11,7 @@ sealed class MethodInfo<TParam, TResp>(
|
||||
open val canThrow: Set<KClass<*>> = emptySet(),
|
||||
open val responseInfo: ResponseInfo<TResp>? = null,
|
||||
open val parameterExamples: Map<String, TParam> = emptyMap(),
|
||||
open val operationId: String? = null
|
||||
) {
|
||||
|
||||
data class GetInfo<TParam, TResp>(
|
||||
@ -21,7 +22,8 @@ sealed class MethodInfo<TParam, TResp>(
|
||||
override val deprecated: Boolean = false,
|
||||
override val securitySchemes: Set<String> = emptySet(),
|
||||
override val canThrow: Set<KClass<*>> = emptySet(),
|
||||
override val parameterExamples: Map<String, TParam> = emptyMap()
|
||||
override val parameterExamples: Map<String, TParam> = emptyMap(),
|
||||
override val operationId: String? = null
|
||||
) : MethodInfo<TParam, TResp>(
|
||||
summary = summary,
|
||||
description = description,
|
||||
@ -30,7 +32,8 @@ sealed class MethodInfo<TParam, TResp>(
|
||||
securitySchemes = securitySchemes,
|
||||
canThrow = canThrow,
|
||||
responseInfo = responseInfo,
|
||||
parameterExamples = parameterExamples
|
||||
parameterExamples = parameterExamples,
|
||||
operationId = operationId
|
||||
)
|
||||
|
||||
data class PostInfo<TParam, TReq, TResp>(
|
||||
@ -42,7 +45,8 @@ sealed class MethodInfo<TParam, TResp>(
|
||||
override val deprecated: Boolean = false,
|
||||
override val securitySchemes: Set<String> = emptySet(),
|
||||
override val canThrow: Set<KClass<*>> = emptySet(),
|
||||
override val parameterExamples: Map<String, TParam> = emptyMap()
|
||||
override val parameterExamples: Map<String, TParam> = emptyMap(),
|
||||
override val operationId: String? = null
|
||||
) : MethodInfo<TParam, TResp>(
|
||||
summary = summary,
|
||||
description = description,
|
||||
@ -51,7 +55,8 @@ sealed class MethodInfo<TParam, TResp>(
|
||||
securitySchemes = securitySchemes,
|
||||
canThrow = canThrow,
|
||||
responseInfo = responseInfo,
|
||||
parameterExamples = parameterExamples
|
||||
parameterExamples = parameterExamples,
|
||||
operationId = operationId
|
||||
)
|
||||
|
||||
data class PutInfo<TParam, TReq, TResp>(
|
||||
@ -63,7 +68,8 @@ sealed class MethodInfo<TParam, TResp>(
|
||||
override val deprecated: Boolean = false,
|
||||
override val securitySchemes: Set<String> = emptySet(),
|
||||
override val canThrow: Set<KClass<*>> = emptySet(),
|
||||
override val parameterExamples: Map<String, TParam> = emptyMap()
|
||||
override val parameterExamples: Map<String, TParam> = emptyMap(),
|
||||
override val operationId: String? = null
|
||||
) : MethodInfo<TParam, TResp>(
|
||||
summary = summary,
|
||||
description = description,
|
||||
@ -71,7 +77,8 @@ sealed class MethodInfo<TParam, TResp>(
|
||||
deprecated = deprecated,
|
||||
securitySchemes = securitySchemes,
|
||||
canThrow = canThrow,
|
||||
parameterExamples = parameterExamples
|
||||
parameterExamples = parameterExamples,
|
||||
operationId = operationId
|
||||
)
|
||||
|
||||
data class DeleteInfo<TParam, TResp>(
|
||||
@ -82,7 +89,8 @@ sealed class MethodInfo<TParam, TResp>(
|
||||
override val deprecated: Boolean = false,
|
||||
override val securitySchemes: Set<String> = emptySet(),
|
||||
override val canThrow: Set<KClass<*>> = emptySet(),
|
||||
override val parameterExamples: Map<String, TParam> = emptyMap()
|
||||
override val parameterExamples: Map<String, TParam> = emptyMap(),
|
||||
override val operationId: String? = null
|
||||
) : MethodInfo<TParam, TResp>(
|
||||
summary = summary,
|
||||
description = description,
|
||||
@ -90,6 +98,7 @@ sealed class MethodInfo<TParam, TResp>(
|
||||
deprecated = deprecated,
|
||||
securitySchemes = securitySchemes,
|
||||
canThrow = canThrow,
|
||||
parameterExamples = parameterExamples
|
||||
parameterExamples = parameterExamples,
|
||||
operationId = operationId
|
||||
)
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
package io.bkbn.kompendium.models.oas
|
||||
|
||||
sealed class OpenApiSpecComponentSchema(open val default: Any? = null) {
|
||||
|
||||
fun addDefault(default: Any?): OpenApiSpecComponentSchema = when (this) {
|
||||
is AnyOfReferencedSchema -> error("Cannot add default to anyOf reference")
|
||||
is ReferencedSchema -> this.copy(default = default)
|
||||
@ -12,7 +11,6 @@ sealed class OpenApiSpecComponentSchema(open val default: Any? = null) {
|
||||
is FormatSchema -> this.copy(default = default)
|
||||
is ArraySchema -> this.copy(default = default)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
sealed class TypedSchema(open val type: String, override val default: Any? = null) : OpenApiSpecComponentSchema(default)
|
||||
|
@ -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
|
||||
|
||||
import io.ktor.routing.PathSegmentConstantRouteSelector
|
||||
import io.ktor.routing.PathSegmentParameterRouteSelector
|
||||
import io.ktor.routing.RootRouteSelector
|
||||
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 {
|
||||
|
||||
/**
|
||||
* Core route calculation method
|
||||
*/
|
||||
fun calculate(route: Route?, tail: String = ""): String
|
||||
private val pathHandler: RouteHandlerMap = mutableMapOf()
|
||||
|
||||
/**
|
||||
* Used to handle any custom selectors that may be missed by the base route calculation
|
||||
*/
|
||||
fun handleCustomSelectors(route: Route, tail: String): String
|
||||
init {
|
||||
addCustomRouteHandler(RootRouteSelector::class) { _, tail -> tail }
|
||||
addCustomRouteHandler(PathSegmentParameterRouteSelector::class) { route, tail ->
|
||||
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>
|
@ -13,14 +13,19 @@ import io.ktor.routing.route
|
||||
/**
|
||||
* Provides an out-of-the-box route to return the generated [OpenApiSpec]
|
||||
* @param oas spec that is returned
|
||||
* @param om provider for Jackson
|
||||
*/
|
||||
fun Routing.openApi(oas: OpenApiSpec) {
|
||||
val om = ObjectMapper()
|
||||
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
|
||||
.enable(SerializationFeature.INDENT_OUTPUT)
|
||||
fun Routing.openApi(
|
||||
oas: OpenApiSpec,
|
||||
om: ObjectMapper = objectMapper
|
||||
) {
|
||||
route("/openapi.json") {
|
||||
get {
|
||||
call.respondText { om.writeValueAsString(oas) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val objectMapper = ObjectMapper()
|
||||
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
|
||||
.enable(SerializationFeature.INDENT_OUTPUT)
|
||||
|
@ -32,6 +32,9 @@ import io.bkbn.kompendium.util.notarizedGetWithNotarizedException
|
||||
import io.bkbn.kompendium.util.notarizedPostModule
|
||||
import io.bkbn.kompendium.util.notarizedPutModule
|
||||
import io.bkbn.kompendium.util.pathParsingTestModule
|
||||
import io.bkbn.kompendium.util.polymorphicCollectionResponse
|
||||
import io.bkbn.kompendium.util.polymorphicInterfaceResponse
|
||||
import io.bkbn.kompendium.util.polymorphicMapResponse
|
||||
import io.bkbn.kompendium.util.polymorphicResponse
|
||||
import io.bkbn.kompendium.util.primitives
|
||||
import io.bkbn.kompendium.util.returnsList
|
||||
@ -40,8 +43,10 @@ import io.bkbn.kompendium.util.simpleGenericResponse
|
||||
import io.bkbn.kompendium.util.statusPageModule
|
||||
import io.bkbn.kompendium.util.statusPageMultiExceptions
|
||||
import io.bkbn.kompendium.util.trailingSlash
|
||||
import io.bkbn.kompendium.util.undeclaredType
|
||||
import io.bkbn.kompendium.util.withDefaultParameter
|
||||
import io.bkbn.kompendium.util.withExamples
|
||||
import io.bkbn.kompendium.util.withOperationId
|
||||
|
||||
internal class KompendiumTest {
|
||||
|
||||
@ -135,7 +140,6 @@ internal class KompendiumTest {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun `Notarized put does not interrupt the pipeline`() {
|
||||
withTestApplication({
|
||||
@ -359,6 +363,22 @@ internal class KompendiumTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Can add operationId`() {
|
||||
withTestApplication({
|
||||
jacksonConfigModule()
|
||||
docs()
|
||||
withOperationId()
|
||||
}) {
|
||||
// do
|
||||
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
|
||||
|
||||
// expect
|
||||
val expected = getFileSnapshot("notarized_get_with_operation_id.json").trim()
|
||||
assertEquals(expected, json, "The received json spec should match the expected content")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Generates the expected redoc`() {
|
||||
withTestApplication({
|
||||
@ -457,6 +477,54 @@ internal class KompendiumTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Can generate a collection with polymorphic response type`() {
|
||||
withTestApplication({
|
||||
jacksonConfigModule()
|
||||
docs()
|
||||
polymorphicCollectionResponse()
|
||||
}) {
|
||||
// do
|
||||
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
|
||||
|
||||
// expect
|
||||
val expected = getFileSnapshot("polymorphic_list_response.json").trim()
|
||||
assertEquals(expected, json, "The received json spec should match the expected content")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Can generate a map with a polymorphic response type`() {
|
||||
withTestApplication({
|
||||
jacksonConfigModule()
|
||||
docs()
|
||||
polymorphicMapResponse()
|
||||
}) {
|
||||
// do
|
||||
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
|
||||
|
||||
// expect
|
||||
val expected = getFileSnapshot("polymorphic_map_response.json").trim()
|
||||
assertEquals(expected, json, "The received json spec should match the expected content")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Can generate a polymorphic response from a sealed interface`() {
|
||||
withTestApplication({
|
||||
jacksonConfigModule()
|
||||
docs()
|
||||
polymorphicInterfaceResponse()
|
||||
}) {
|
||||
// do
|
||||
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
|
||||
|
||||
// expect
|
||||
val expected = getFileSnapshot("sealed_interface_response.json").trim()
|
||||
assertEquals(expected, json, "The received json spec should match the expected content")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Can generate a response type with a generic type`() {
|
||||
withTestApplication({
|
||||
@ -505,6 +573,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(
|
||||
info = OpenApiSpecInfo(
|
||||
title = "Test API",
|
||||
@ -539,5 +623,4 @@ internal class KompendiumTest {
|
||||
redoc(oas)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -13,15 +13,7 @@ import io.bkbn.kompendium.models.oas.DictionarySchema
|
||||
import io.bkbn.kompendium.models.oas.FormatSchema
|
||||
import io.bkbn.kompendium.models.oas.ObjectSchema
|
||||
import io.bkbn.kompendium.models.oas.ReferencedSchema
|
||||
import io.bkbn.kompendium.util.ComplexRequest
|
||||
import io.bkbn.kompendium.util.TestInvalidMap
|
||||
import io.bkbn.kompendium.util.TestNestedModel
|
||||
import io.bkbn.kompendium.util.TestSimpleModel
|
||||
import io.bkbn.kompendium.util.TestSimpleWithEnumList
|
||||
import io.bkbn.kompendium.util.TestSimpleWithEnums
|
||||
import io.bkbn.kompendium.util.TestSimpleWithList
|
||||
import io.bkbn.kompendium.util.TestSimpleWithMap
|
||||
import io.bkbn.kompendium.util.TestWithUUID
|
||||
import io.bkbn.kompendium.util.*
|
||||
|
||||
@ExperimentalStdlibApi
|
||||
internal class KontentTest {
|
||||
@ -45,6 +37,29 @@ internal class KontentTest {
|
||||
assertEquals(FormatSchema("int64", "integer"), result["Long"])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Object with BigDecimal and BigInteger types`() {
|
||||
// do
|
||||
val result = generateKontent<TestBigNumberModel>()
|
||||
|
||||
// expect
|
||||
assertEquals(3, result.count())
|
||||
assertTrue { result.containsKey(TestBigNumberModel::class.simpleName) }
|
||||
assertEquals(FormatSchema("double", "number"), result["BigDecimal"])
|
||||
assertEquals(FormatSchema("int64", "integer"), result["BigInteger"])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Object with ByteArray type`() {
|
||||
// do
|
||||
val result = generateKontent<TestByteArrayModel>()
|
||||
|
||||
// expect
|
||||
assertEquals(2, result.count())
|
||||
assertTrue { result.containsKey(TestByteArrayModel::class.simpleName) }
|
||||
assertEquals(FormatSchema("byte", "string"), result["ByteArray"])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Objects reference their base types in the cache`() {
|
||||
// do
|
||||
|
@ -4,9 +4,16 @@ import java.util.UUID
|
||||
import io.bkbn.kompendium.annotations.KompendiumField
|
||||
import io.bkbn.kompendium.annotations.KompendiumParam
|
||||
import io.bkbn.kompendium.annotations.ParamType
|
||||
import io.bkbn.kompendium.annotations.UndeclaredField
|
||||
import java.math.BigDecimal
|
||||
import java.math.BigInteger
|
||||
|
||||
data class TestSimpleModel(val a: String, val b: Int)
|
||||
|
||||
data class TestBigNumberModel(val a: BigDecimal, val b: BigInteger)
|
||||
|
||||
data class TestByteArrayModel(val a: ByteArray)
|
||||
|
||||
data class TestNestedModel(val inner: TestSimpleModel)
|
||||
|
||||
data class TestSimpleWithEnums(val a: String, val b: SimpleEnum)
|
||||
@ -80,7 +87,21 @@ sealed class FlibbityGibbit
|
||||
data class SimpleGibbit(val a: String) : FlibbityGibbit()
|
||||
data class ComplexGibbit(val b: String, val c: Int) : FlibbityGibbit()
|
||||
|
||||
sealed interface SlammaJamma
|
||||
|
||||
data class OneJamma(val a: Int) : SlammaJamma
|
||||
data class AnothaJamma(val b: Float) : SlammaJamma
|
||||
//data class InsaneJamma(val c: SlammaJamma) : SlammaJamma // 👀
|
||||
|
||||
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>
|
||||
|
||||
enum class Hehe {
|
||||
HAHA,
|
||||
HOHO
|
||||
}
|
||||
|
||||
@UndeclaredField("nowYouDont", Hehe::class)
|
||||
data class Mysterious(val nowYouSeeMe: String)
|
||||
|
@ -265,6 +265,18 @@ fun Application.withDefaultParameter() {
|
||||
}
|
||||
}
|
||||
|
||||
fun Application.withOperationId(){
|
||||
routing {
|
||||
route("/test") {
|
||||
notarizedGet(
|
||||
info = TestResponseInfo.testGetInfo.copy(operationId = "getTest")
|
||||
){
|
||||
call.respond(HttpStatusCode.OK)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Application.nonRequiredParamsGet() {
|
||||
routing {
|
||||
route("/test/optional") {
|
||||
@ -285,6 +297,36 @@ fun Application.polymorphicResponse() {
|
||||
}
|
||||
}
|
||||
|
||||
fun Application.polymorphicCollectionResponse() {
|
||||
routing {
|
||||
route("/test/polymorphiclist") {
|
||||
notarizedGet(TestResponseInfo.polymorphicListResponse) {
|
||||
call.respond(HttpStatusCode.OK, listOf(SimpleGibbit("hi")))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Application.polymorphicMapResponse() {
|
||||
routing {
|
||||
route("/test/polymorphicmap") {
|
||||
notarizedGet(TestResponseInfo.polymorphicMapResponse) {
|
||||
call.respond(HttpStatusCode.OK, listOf(SimpleGibbit("hi")))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Application.polymorphicInterfaceResponse() {
|
||||
routing {
|
||||
route("/test/polymorphicmap") {
|
||||
notarizedGet(TestResponseInfo.polymorphicInterfaceResponse) {
|
||||
call.respond(HttpStatusCode.OK, listOf(SimpleGibbit("hi")))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Application.genericPolymorphicResponse() {
|
||||
routing {
|
||||
route("/test/polymorphic") {
|
||||
@ -310,6 +352,16 @@ fun Application.genericPolymorphicResponseMultipleImpls() {
|
||||
}
|
||||
}
|
||||
|
||||
fun Application.undeclaredType() {
|
||||
routing {
|
||||
route("/test/polymorphic") {
|
||||
notarizedGet(TestResponseInfo.undeclaredResponseType) {
|
||||
call.respond(HttpStatusCode.OK, Mysterious("hi"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Application.simpleGenericResponse() {
|
||||
routing {
|
||||
route("/test/polymorphic") {
|
||||
|
@ -78,6 +78,21 @@ object TestResponseInfo {
|
||||
description = "Polymorphic response",
|
||||
responseInfo = simpleOkResponse()
|
||||
)
|
||||
val polymorphicListResponse = GetInfo<Unit, List<FlibbityGibbit>>(
|
||||
summary = "Oh so many gibbits",
|
||||
description = "Polymorphic list response",
|
||||
responseInfo = simpleOkResponse()
|
||||
)
|
||||
val polymorphicMapResponse = GetInfo<Unit, Map<String, FlibbityGibbit>>(
|
||||
summary = "By gawd that's a lot of gibbits",
|
||||
description = "Polymorphic list response",
|
||||
responseInfo = simpleOkResponse()
|
||||
)
|
||||
val polymorphicInterfaceResponse = GetInfo<Unit, SlammaJamma>(
|
||||
summary = "Come on and slam",
|
||||
description = "and welcome to the jam",
|
||||
responseInfo = simpleOkResponse()
|
||||
)
|
||||
val genericPolymorphicResponse = GetInfo<Unit, Flibbity<TestNested>>(
|
||||
summary = "More flibbity",
|
||||
description = "Polymorphic with generics",
|
||||
@ -88,6 +103,11 @@ object TestResponseInfo {
|
||||
description = "Polymorphic with generics but like... crazier",
|
||||
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>>(
|
||||
summary = "Single Generic",
|
||||
description = "Simple generic data class",
|
||||
|
@ -0,0 +1,88 @@
|
||||
{
|
||||
"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" : {
|
||||
"get" : {
|
||||
"tags" : [ ],
|
||||
"summary" : "Another get test",
|
||||
"description" : "testing more",
|
||||
"operationId" : "getTest",
|
||||
"parameters" : [ {
|
||||
"name" : "a",
|
||||
"in" : "path",
|
||||
"schema" : {
|
||||
"type" : "string"
|
||||
},
|
||||
"required" : true,
|
||||
"deprecated" : false
|
||||
}, {
|
||||
"name" : "aa",
|
||||
"in" : "query",
|
||||
"schema" : {
|
||||
"type" : "integer",
|
||||
"format" : "int32"
|
||||
},
|
||||
"required" : true,
|
||||
"deprecated" : false
|
||||
} ],
|
||||
"responses" : {
|
||||
"200" : {
|
||||
"description" : "A Successful Endeavor",
|
||||
"content" : {
|
||||
"application/json" : {
|
||||
"schema" : {
|
||||
"$ref" : "#/components/schemas/TestResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"deprecated" : false
|
||||
}
|
||||
}
|
||||
},
|
||||
"components" : {
|
||||
"schemas" : {
|
||||
"String" : {
|
||||
"type" : "string"
|
||||
},
|
||||
"TestResponse" : {
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
"c" : {
|
||||
"$ref" : "#/components/schemas/String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Int" : {
|
||||
"type" : "integer",
|
||||
"format" : "int32"
|
||||
}
|
||||
},
|
||||
"securitySchemes" : { }
|
||||
},
|
||||
"security" : [ ],
|
||||
"tags" : [ ]
|
||||
}
|
@ -0,0 +1,91 @@
|
||||
{
|
||||
"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/polymorphiclist" : {
|
||||
"get" : {
|
||||
"tags" : [ ],
|
||||
"summary" : "Oh so many gibbits",
|
||||
"description" : "Polymorphic list response",
|
||||
"parameters" : [ ],
|
||||
"responses" : {
|
||||
"200" : {
|
||||
"description" : "A successful endeavor",
|
||||
"content" : {
|
||||
"application/json" : {
|
||||
"schema" : {
|
||||
"$ref" : "#/components/schemas/List-FlibbityGibbit"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"deprecated" : false
|
||||
}
|
||||
}
|
||||
},
|
||||
"components" : {
|
||||
"schemas" : {
|
||||
"String" : {
|
||||
"type" : "string"
|
||||
},
|
||||
"SimpleGibbit" : {
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
"a" : {
|
||||
"$ref" : "#/components/schemas/String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Int" : {
|
||||
"type" : "integer",
|
||||
"format" : "int32"
|
||||
},
|
||||
"ComplexGibbit" : {
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
"b" : {
|
||||
"$ref" : "#/components/schemas/String"
|
||||
},
|
||||
"c" : {
|
||||
"$ref" : "#/components/schemas/Int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"List-FlibbityGibbit" : {
|
||||
"type" : "array",
|
||||
"items" : {
|
||||
"anyOf" : [ {
|
||||
"$ref" : "#/components/schemas/SimpleGibbit"
|
||||
}, {
|
||||
"$ref" : "#/components/schemas/ComplexGibbit"
|
||||
} ]
|
||||
}
|
||||
}
|
||||
},
|
||||
"securitySchemes" : { }
|
||||
},
|
||||
"security" : [ ],
|
||||
"tags" : [ ]
|
||||
}
|
@ -0,0 +1,91 @@
|
||||
{
|
||||
"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/polymorphicmap" : {
|
||||
"get" : {
|
||||
"tags" : [ ],
|
||||
"summary" : "By gawd that's a lot of gibbits",
|
||||
"description" : "Polymorphic list response",
|
||||
"parameters" : [ ],
|
||||
"responses" : {
|
||||
"200" : {
|
||||
"description" : "A successful endeavor",
|
||||
"content" : {
|
||||
"application/json" : {
|
||||
"schema" : {
|
||||
"$ref" : "#/components/schemas/Map-String-FlibbityGibbit"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"deprecated" : false
|
||||
}
|
||||
}
|
||||
},
|
||||
"components" : {
|
||||
"schemas" : {
|
||||
"String" : {
|
||||
"type" : "string"
|
||||
},
|
||||
"SimpleGibbit" : {
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
"a" : {
|
||||
"$ref" : "#/components/schemas/String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Int" : {
|
||||
"type" : "integer",
|
||||
"format" : "int32"
|
||||
},
|
||||
"ComplexGibbit" : {
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
"b" : {
|
||||
"$ref" : "#/components/schemas/String"
|
||||
},
|
||||
"c" : {
|
||||
"$ref" : "#/components/schemas/Int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Map-String-FlibbityGibbit" : {
|
||||
"type" : "object",
|
||||
"additionalProperties" : {
|
||||
"anyOf" : [ {
|
||||
"$ref" : "#/components/schemas/SimpleGibbit"
|
||||
}, {
|
||||
"$ref" : "#/components/schemas/ComplexGibbit"
|
||||
} ]
|
||||
}
|
||||
}
|
||||
},
|
||||
"securitySchemes" : { }
|
||||
},
|
||||
"security" : [ ],
|
||||
"tags" : [ ]
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
{
|
||||
"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/polymorphicmap" : {
|
||||
"get" : {
|
||||
"tags" : [ ],
|
||||
"summary" : "Come on and slam",
|
||||
"description" : "and welcome to the jam",
|
||||
"parameters" : [ ],
|
||||
"responses" : {
|
||||
"200" : {
|
||||
"description" : "A successful endeavor",
|
||||
"content" : {
|
||||
"application/json" : {
|
||||
"schema" : {
|
||||
"anyOf" : [ {
|
||||
"$ref" : "#/components/schemas/OneJamma"
|
||||
}, {
|
||||
"$ref" : "#/components/schemas/AnothaJamma"
|
||||
} ]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"deprecated" : false
|
||||
}
|
||||
}
|
||||
},
|
||||
"components" : {
|
||||
"schemas" : {
|
||||
"Int" : {
|
||||
"type" : "integer",
|
||||
"format" : "int32"
|
||||
},
|
||||
"OneJamma" : {
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
"a" : {
|
||||
"$ref" : "#/components/schemas/Int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Float" : {
|
||||
"type" : "number",
|
||||
"format" : "float"
|
||||
},
|
||||
"AnothaJamma" : {
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
"b" : {
|
||||
"$ref" : "#/components/schemas/Float"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"securitySchemes" : { }
|
||||
},
|
||||
"security" : [ ],
|
||||
"tags" : [ ]
|
||||
}
|
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" : [ ]
|
||||
}
|
78
kompendium-locations/build.gradle.kts
Normal file
78
kompendium-locations/build.gradle.kts
Normal 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)
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
@ -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()
|
||||
}
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
package io.bkbn.kompendium.locations.util
|
||||
|
@ -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" : [ ]
|
||||
}
|
@ -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" : [ ]
|
||||
}
|
@ -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" : [ ]
|
||||
}
|
@ -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" : [ ]
|
||||
}
|
@ -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" : [ ]
|
||||
}
|
@ -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" : [ ]
|
||||
}
|
@ -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" : [ ]
|
||||
}
|
@ -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" : [ ]
|
||||
}
|
@ -9,6 +9,7 @@ dependencies {
|
||||
|
||||
implementation(projects.kompendiumCore)
|
||||
implementation(projects.kompendiumAuth)
|
||||
implementation(projects.kompendiumLocations)
|
||||
implementation(projects.kompendiumSwaggerUi)
|
||||
|
||||
implementation(libs.bundles.ktor)
|
||||
@ -16,8 +17,11 @@ dependencies {
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
implementation(libs.ktor.serialization)
|
||||
implementation(libs.bundles.ktorAuth)
|
||||
implementation(libs.ktor.locations)
|
||||
implementation(libs.bundles.logging)
|
||||
|
||||
implementation("joda-time:joda-time:2.10.13")
|
||||
|
||||
testImplementation("org.jetbrains.kotlin:kotlin-test")
|
||||
testImplementation("org.jetbrains.kotlin:kotlin-test-junit")
|
||||
}
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
package io.bkbn.kompendium.playground
|
||||
|
||||
import io.bkbn.kompendium.Kompendium
|
||||
import io.bkbn.kompendium.Notarized.notarizedDelete
|
||||
import io.bkbn.kompendium.Notarized.notarizedException
|
||||
import io.bkbn.kompendium.Notarized.notarizedGet
|
||||
@ -7,7 +8,9 @@ import io.bkbn.kompendium.Notarized.notarizedPost
|
||||
import io.bkbn.kompendium.Notarized.notarizedPut
|
||||
import io.bkbn.kompendium.auth.KompendiumAuth.notarizedBasic
|
||||
import io.bkbn.kompendium.models.meta.ResponseInfo
|
||||
import io.bkbn.kompendium.models.oas.FormatSchema
|
||||
import io.bkbn.kompendium.playground.PlaygroundToC.testAuthenticatedSingleGetInfo
|
||||
import io.bkbn.kompendium.playground.PlaygroundToC.testCustomOverride
|
||||
import io.bkbn.kompendium.playground.PlaygroundToC.testGetWithExamples
|
||||
import io.bkbn.kompendium.playground.PlaygroundToC.testIdGetInfo
|
||||
import io.bkbn.kompendium.playground.PlaygroundToC.testPostWithExamples
|
||||
@ -16,6 +19,7 @@ import io.bkbn.kompendium.playground.PlaygroundToC.testSingleGetInfo
|
||||
import io.bkbn.kompendium.playground.PlaygroundToC.testSingleGetInfoWithThrowable
|
||||
import io.bkbn.kompendium.playground.PlaygroundToC.testSinglePostInfo
|
||||
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.redoc
|
||||
import io.bkbn.kompendium.swagger.swaggerUI
|
||||
@ -36,8 +40,11 @@ import io.ktor.serialization.json
|
||||
import io.ktor.server.engine.embeddedServer
|
||||
import io.ktor.server.netty.Netty
|
||||
import io.ktor.webjars.Webjars
|
||||
import org.joda.time.DateTime
|
||||
|
||||
fun main() {
|
||||
Kompendium.addCustomTypeSchema(DateTime::class, FormatSchema("date-time", "string"))
|
||||
|
||||
embeddedServer(
|
||||
Netty,
|
||||
port = 8081,
|
||||
@ -45,9 +52,9 @@ fun main() {
|
||||
).start(wait = true)
|
||||
}
|
||||
|
||||
var featuresInstalled = false
|
||||
private var featuresInstalled = false
|
||||
|
||||
fun Application.configModule() {
|
||||
private fun Application.configModule() {
|
||||
if (!featuresInstalled) {
|
||||
install(ContentNegotiation) {
|
||||
json()
|
||||
@ -80,7 +87,7 @@ fun Application.configModule() {
|
||||
}
|
||||
}
|
||||
|
||||
fun Application.mainModule() {
|
||||
private fun Application.mainModule() {
|
||||
configModule()
|
||||
routing {
|
||||
openApi(oas)
|
||||
@ -114,6 +121,11 @@ fun Application.mainModule() {
|
||||
call.respondText { "heya" }
|
||||
}
|
||||
}
|
||||
route("custom_override") {
|
||||
notarizedGet(testCustomOverride) {
|
||||
call.respondText { DateTime.now().toString() }
|
||||
}
|
||||
}
|
||||
authenticate("basic") {
|
||||
route("/authenticated/single") {
|
||||
notarizedGet(testAuthenticatedSingleGetInfo) {
|
||||
@ -127,5 +139,10 @@ fun Application.mainModule() {
|
||||
error("bad things just happened")
|
||||
}
|
||||
}
|
||||
route("/undeclared") {
|
||||
notarizedGet(testUndeclaredFields) {
|
||||
call.respondText { "hi" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,9 @@ package io.bkbn.kompendium.playground
|
||||
import io.bkbn.kompendium.annotations.KompendiumField
|
||||
import io.bkbn.kompendium.annotations.KompendiumParam
|
||||
import io.bkbn.kompendium.annotations.ParamType
|
||||
import io.bkbn.kompendium.annotations.UndeclaredField
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.joda.time.DateTime
|
||||
|
||||
data class ExampleParams(
|
||||
@KompendiumParam(ParamType.PATH) val id: Int,
|
||||
@ -25,8 +28,19 @@ data class ExampleRequest(
|
||||
val aaa: List<Long>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ExampleResponse(val c: String)
|
||||
|
||||
data class ExceptionResponse(val message: String)
|
||||
|
||||
data class ExampleCreatedResponse(val id: Int, val c: String)
|
||||
|
||||
data class DateTimeWrapper(val dt: DateTime)
|
||||
|
||||
enum class Testerino {
|
||||
First,
|
||||
Second
|
||||
}
|
||||
|
||||
@UndeclaredField("type", Testerino::class)
|
||||
data class SimpleYetMysterious(val exists: Boolean)
|
||||
|
@ -55,6 +55,15 @@ object PlaygroundToC {
|
||||
description = "Returns a different sample"
|
||||
)
|
||||
)
|
||||
val testCustomOverride = MethodInfo.GetInfo<Unit, DateTimeWrapper>(
|
||||
summary = "custom schema test",
|
||||
description = "testing",
|
||||
tags = setOf("custom"),
|
||||
responseInfo = ResponseInfo(
|
||||
status = HttpStatusCode.OK,
|
||||
description = "good tings"
|
||||
)
|
||||
)
|
||||
val testSingleGetInfoWithThrowable = testSingleGetInfo.copy(
|
||||
summary = "Show me the error baby 🙏",
|
||||
canThrow = setOf(Exception::class)
|
||||
@ -100,4 +109,13 @@ object PlaygroundToC {
|
||||
),
|
||||
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,8 +1,10 @@
|
||||
rootProject.name = "kompendium"
|
||||
|
||||
include("kompendium-core")
|
||||
include("kompendium-auth")
|
||||
include("kompendium-swagger-ui")
|
||||
include("kompendium-playground")
|
||||
include("kompendium-locations")
|
||||
|
||||
// Feature Previews
|
||||
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
|
||||
|
Reference in New Issue
Block a user