Compare commits

..

13 Commits

Author SHA1 Message Date
aa1b898b22 fix: adding signing plugin (#147) 2022-01-12 11:19:19 -05:00
aa21c1219b fix: bug in testing suite + lots of documentation updates 2022-01-12 09:15:27 -05:00
bc380077fb chore(deps): update plugin org.jetbrains.kotlin.plugin.serialization to v1.6.10 (#143) 2022-01-09 13:09:51 +00:00
fc9929e9cc chore(deps): update plugin io.bkbn.sourdough.root to v0.3.3 (#142) 2022-01-09 08:05:52 -05:00
a26ad72b67 chore(deps): update dependency org.jetbrains.kotlinx:kotlinx-serialization-json to v1.3.2 (#141)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2022-01-09 11:24:03 +00:00
38a70e4979 chore(deps): update dependency org.apache.logging.log4j:log4j-core to v2.17.1 (#140)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2022-01-09 09:47:01 +00:00
4a1425b73b chore(deps): update dependency org.apache.logging.log4j:log4j-api to v2.17.1 (#138) 2022-01-09 03:13:12 +00:00
01c118373e chore(deps): update dependency gradle to v7.3.3 (#137) 2022-01-08 22:09:03 -05:00
7535d67661 chore(deps): add renovate.json 2022-01-09 02:58:31 +00:00
eb369dcdc8 fix: locations inheritance (#135) 2022-01-07 08:46:20 -05:00
da104d0a63 feat: Multi Serialization Support (#134) 2022-01-04 02:05:30 +00:00
c6ed261fe4 feat: enable creation of explicit parameter examples (#133) 2022-01-03 10:34:02 -05:00
012db5ad26 feat: added head, patch, and options methods (#132) 2022-01-03 14:32:55 +00:00
98 changed files with 1574 additions and 537 deletions

View File

@ -15,9 +15,6 @@ jobs:
with:
gradle-version: wrapper
arguments: detekt
properties: |
org.gradle.vfs.watch=false
org.gradle.vfs.verbose=false
unit:
runs-on: ubuntu-latest
steps:
@ -32,6 +29,3 @@ jobs:
with:
gradle-version: wrapper
arguments: test koverCollectReports
properties: |
org.gradle.vfs.watch=false
org.gradle.vfs.verbose=false

View File

@ -2,6 +2,8 @@ name: Publish to GitHub Packages
on:
push:
branches: [ main ]
paths-ignore:
- docs/**
env:
ORG_GRADLE_PROJECT_signingKey: ${{ secrets.SONATYPE_SIGNING_KEY }}
ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.SONATYPE_SIGNING_PASSWORD }}
@ -19,8 +21,5 @@ jobs:
with:
gradle-version: wrapper
arguments: publishAllPublicationsToGithubPackagesRepository
properties: |
org.gradle.vfs.watch=false
org.gradle.vfs.verbose=false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -8,23 +8,6 @@ env:
ORG_GRADLE_PROJECT_signingKey: ${{ secrets.SONATYPE_SIGNING_KEY }}
ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.SONATYPE_SIGNING_PASSWORD }}
jobs:
publish-to-github:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-java@v2
with:
distribution: 'adopt'
java-version: '17'
- name: Publish to GitHub Packages
uses: burrunan/gradle-cache-action@v1
with:
gradle-version: wrapper
arguments: publishAllPublicationsToGithubPackagesRepository
properties: |
release=true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
publish-to-nexus:
runs-on: ubuntu-latest
steps:
@ -43,3 +26,28 @@ jobs:
env:
ORG_GRADLE_PROJECT_sonatypeUsername: ${{ secrets.SONATYPE_USER }}
ORG_GRADLE_PROJECT_sonatypePassword: ${{ secrets.SONATYPE_PASSWORD }}
build-documentation:
runs-on: ubuntu-latest
needs:
- publish-to-nexus
steps:
- uses: actions/checkout@v2
with:
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
- uses: actions/setup-java@v2
with:
distribution: 'adopt'
java-version: '17'
- name: Build New Documentation
uses: burrunan/gradle-cache-action@v1
with:
gradle-version: wrapper
arguments: dokkaHtmlMultiModule
properties: |
release=true
- name: Push New Documentation
uses: EndBug/add-and-commit@v7
with:
default_author: github_actions
branch: main
message: 'doc: Added Latest Documentation ✨'

View File

@ -11,7 +11,20 @@
## Released
## [2.0.0-alpha] - January 2nd, 2021
## [2.0.0-beta] - January 12th, 2022
### Added
- Support for HTTP Patch, Head, and Options methods
- Support for including parameter examples via `MethodInfo`
- Dokka Pipeline Generation
- GitHub Pages integration
- Sourdough Gradle updates
### Changed
- Kompendium now leverages the chosen API serializer. Supports Jackson, Gson and Kotlinx Serialization
- Fixed bug where overridden field names were not reflected in serialized object and required array
- Fixed bug where Ktor Location parents were not being scanned for parameters
## [2.0.0-alpha] - January 2nd, 2022
### Added
- Support for OAuth authentication
- Gradle Toolchain feature to ensure match between local JDK and compile target

41
Project.md Normal file
View File

@ -0,0 +1,41 @@
# Kompendium
Welcome to Kompendium, the straight-forward, minimally-invasive OpenAPI generator for Ktor.
## How to install
Kompendium publishes all releases to Maven Central. As such, using the release versions of `Kompendium` is as simple as
declaring it as an implementation dependency in your `build.gradle.kts`
```kotlin
repositories {
mavenCentral()
}
dependencies {
implementation("io.bkbn:kompendium-core:latest.release")
}
```
In addition to publishing releases to Maven Central, a snapshot version gets published to GitHub Packages on every merge
to `main`. These can be consumed by adding the repository to your gradle build file. Instructions can be
found [here](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-gradle-registry#using-a-published-package)
## Setting up the Kompendium Plugin
Kompendium is instantiated as a Ktor Feature/Plugin. It can be added to your API as follows
```kotlin
private fun Application.mainModule() {
// Installs the Kompendium Plugin and sets up baseline server metadata
install(Kompendium) {
spec = OpenApiSpec(/*..*/)
}
// ...
}
```
## Notarization
The concept of notarizing routes / exceptions / etc. is central to Kompendium. More details on _how_ to notarize your
API can be found in the kompendium-core module.

View File

@ -31,15 +31,13 @@ dependencies {
}
```
The last two dependencies are optional.
In addition to publishing releases to Maven Central, a snapshot version gets published to GitHub Packages on every merge
to `main`. These can be consumed by adding the repository to your gradle build file. Instructions can be
found [here](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-gradle-registry#using-a-published-package)
# Library Details
Details on how to use Kompendium can be found in the Wiki (WIP, this is new for V2 and will be fleshed out prior to release)
Forthcoming, more details on V2 will be published soon :)
## Local Development

View File

@ -1,14 +1,10 @@
import io.bkbn.sourdough.gradle.core.extension.SourdoughLibraryExtension
plugins {
id("io.bkbn.sourdough.root") version "0.3.0"
kotlin("jvm") version "1.6.10" apply false
id("io.bkbn.sourdough.root") version "0.5.4"
id("com.github.jakemarsden.git-hooks") version "0.0.2"
}
sourdough {
toolChainJavaVersion.set(JavaLanguageVersion.of(JavaVersion.VERSION_17.majorVersion))
jvmTarget.set(JavaVersion.VERSION_11.majorVersion)
compilerArgs.set(listOf("-opt-in=kotlin.RequiresOptIn"))
id("org.jetbrains.dokka") version "1.6.10"
id("org.jetbrains.kotlinx.kover") version "0.5.0-RC"
id("io.github.gradle-nexus.publish-plugin") version "1.1.0"
}
gitHooks {
@ -31,21 +27,3 @@ allprojects {
}
}
}
subprojects {
if (this.name != "kompendium-playground") {
apply(plugin = "io.bkbn.sourdough.library")
configure<SourdoughLibraryExtension> {
githubOrg.set("bkbnio")
githubRepo.set("kompendium")
libraryName.set("Kompendium")
libraryDescription.set("A minimally invasive OpenAPI spec generator for Ktor")
licenseName.set("MIT License")
licenseUrl.set("https://mit-license.org")
developerId.set("bkbnio")
developerName.set("Ryan Brink")
developerEmail.set("admin@bkbn.io")
}
}
}

View File

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

View File

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@ -7,3 +7,7 @@ It is separated from core predominantly to allow for potential future integratio
# Package io.bkbn.kompendium.annotations
Contains all annotations used by Kompendium
# Package io.bkbn.kompendium.annotations.constraint
Annotations that place bespoke constraints on individual fields of your API schemas.

View File

@ -1,3 +1,30 @@
plugins {
id("io.bkbn.sourdough.library")
kotlin("jvm")
id("io.bkbn.sourdough.library.jvm") version "0.5.4"
id("io.gitlab.arturbosch.detekt") version "1.19.0"
id("com.adarshr.test-logger") version "3.1.0"
id("org.jetbrains.dokka")
id("maven-publish")
id("java-library")
id("signing")
}
sourdough {
githubOrg.set("bkbnio")
githubRepo.set("kompendium")
libraryName.set("Kompendium Annotations")
libraryDescription.set("A set of annotations used by Kompendium to generate OpenAPI Specifications")
licenseName.set("MIT License")
licenseUrl.set("https://mit-license.org")
developerId.set("unredundant")
developerName.set("Ryan Brink")
developerEmail.set("admin@bkbn.io")
}
testing {
suites {
named("test", JvmTestSuite::class) {
useJUnitJupiter()
}
}
}

View File

@ -5,5 +5,9 @@ their API authentication with minimal modifications to their existing configurat
# Package io.bkbn.kompendium.auth
Base package that is responsible for setting up required authentication route handlers along with exposing
wrapper methods for each ktor-auth authentication mechanism.
Base package that is responsible for setting up required authentication route handlers along with exposing wrapper
methods for each ktor-auth authentication mechanism.
# Package io.bkbn.kompendium.auth
Houses the available security configurations. At the moment, `Basic`, `JWT`, `ApiKey`, and `OAuth` are supported

View File

@ -1,7 +1,25 @@
plugins {
id("io.bkbn.sourdough.library")
kotlin("jvm")
id("io.bkbn.sourdough.library.jvm") version "0.5.4"
id("io.gitlab.arturbosch.detekt") version "1.19.0"
id("com.adarshr.test-logger") version "3.1.0"
id("org.jetbrains.dokka")
id("maven-publish")
id("java-library")
id("signing")
}
sourdough {
githubOrg.set("bkbnio")
githubRepo.set("kompendium")
libraryName.set("Kompendium Authentication")
libraryDescription.set("Kompendium library to pair with Ktor Auth to provide authorization info to OpenAPI")
licenseName.set("MIT License")
licenseUrl.set("https://mit-license.org")
developerId.set("unredundant")
developerName.set("Ryan Brink")
developerEmail.set("admin@bkbn.io")
}
dependencies {
// IMPLEMENTATION
@ -16,3 +34,11 @@ dependencies {
testImplementation(testFixtures(projects.kompendiumCore))
}
testing {
suites {
named("test", JvmTestSuite::class) {
useJUnitJupiter()
}
}
}

View File

@ -1,9 +1,100 @@
# Module kompendium-core
This is where the magic happens. This module houses all the reflective goodness that powers Kompendium.
This is where the magic happens. This module houses all the reflective goodness that powers Kompendium.
It is also the only mandatory client-facing module for a basic setup.
# Package io.bkbn.kompendium.core
The root package contains several objects that power Kompendium
The root package contains several objects that power Kompendium, including the Kompendium Ktor Plugin, route
notarization methods, and the reflection engine that analyzes method info type parameters.
## Plugin
The Kompendium plugin is an extremely light-weight plugin, with only a couple areas of customization.
### Serialization
Kompendium relies on your API to provide a properly-configured `ContentNegotiator` in order to convert the `OpenApiSpec`
into JSON. The advantage to this approach is that all of your data classes will be serialized precisely how you define.
The downside is that issues could exist in serialization frameworks that have not been tested. At the moment, Jackson,
Gson and KotlinX serialization have all been tested. If you run into any serialization issues, particularly with a
serializer not listed above, please open an issue on GitHub 🙏
## Notarization
Central to Kompendium is the concept of notarization.
Notarizing a route is the mechanism by which Kompendium analyzes your route types, along with provided metadata, and
converts to the expected OpenAPI format.
Before jumping into notarization, lets first look at a standard Ktor route
```kotlin
routing {
get {
call.respond(HttpStatusCode.OK, BasicResponse(c = UUID.randomUUID().toString()))
}
}
```
Now, let's compare this to the same functionality, but notarized using Kompendium
```kotlin
routing {
notarizedGet(simpleGetExample) {
call.respond(HttpStatusCode.OK, BasicResponse(c = UUID.randomUUID().toString()))
}
}
```
Pretty simple huh. But hold on... what is this `simpleGetExample`? How can I know that it is so "simple". Let's take a
look
```kotlin
val simpleGetExample = GetInfo<Unit, BasicResponse>(
summary = "Simple, Documented GET Request",
description = "This is to showcase just how easy it is to document your Ktor API!",
responseInfo = ResponseInfo(
status = HttpStatusCode.OK,
description = "This means everything went as expected!",
examples = mapOf("demo" to BasicResponse(c = "52c099d7-8642-46cc-b34e-22f39b923cf4"))
),
tags = setOf("Simple")
)
```
See, not so bad 😄 `GetInfo<*,*>` is an implementation of `MethodInfo<TParam, TResp>`, a sealed interface designed to
encapsulate all the metadata required for documenting an API route. Kompendium leverages this data, along with the
provided type parameters `TParam` and `TResp` to construct the full OpenAPI Specification for your route.
Additionally, just as a backup, each notarization method includes a "post-processing' hook that will allow you to have
final say in the generated route info prior to being attached to the spec. This can be accessed via the optional
parameter
```kotlin
routing {
notarizedGet(simpleGetExample, postProcess = { spec -> spec }) {
call.respond(HttpStatusCode.OK, BasicResponse(c = UUID.randomUUID().toString()))
}
}
```
This should only be used in _extremely_ rare scenarios, but it is nice to know it is there if you need it.
# Package io.bkbn.kompendium.core.metadata
Houses all interfaces and types related to describing route metadata.
# Package io.bkbn.kompendium.core.parser
Responsible for the parse of method information. Base implementation is an interface to support extensibility as shown
in the `kompendium-locations` module.
# Package io.bkbn.kompendium.core.routes
Houses any routes provided by the core module. At the moment the only supported route is to enable ReDoc support.
# Package io.bkbn.kompendium.core.util
Collection of utility functions used by Kompendium

View File

@ -1,21 +1,41 @@
plugins {
id("io.bkbn.sourdough.library")
`java-test-fixtures`
kotlin("jvm")
id("io.bkbn.sourdough.library.jvm") version "0.5.4"
id("io.gitlab.arturbosch.detekt") version "1.19.0"
id("com.adarshr.test-logger") version "3.1.0"
id("org.jetbrains.dokka")
id("maven-publish")
id("java-library")
id("signing")
id("java-test-fixtures")
}
sourdough {
githubOrg.set("bkbnio")
githubRepo.set("kompendium")
libraryName.set("Kompendium Core")
libraryDescription.set("Core functionality for the Kompendium library")
licenseName.set("MIT License")
licenseUrl.set("https://mit-license.org")
developerId.set("unredundant")
developerName.set("Ryan Brink")
developerEmail.set("admin@bkbn.io")
compilerArgs.set(listOf("-opt-in=kotlin.RequiresOptIn"))
}
dependencies {
// VERSIONS
val ktorVersion: String by project
val kotestVersion: String by project
// IMPLEMENTATION
api(projects.kompendiumOas)
api(projects.kompendiumAnnotations)
val ktorVersion: String by project
val kotestVersion: String by project
implementation(group = "io.ktor", name = "ktor-server-core", version = ktorVersion)
implementation(group = "io.ktor", name = "ktor-html-builder", version = ktorVersion)
implementation(group = "com.fasterxml.jackson.module", name = "jackson-module-kotlin", version = "2.13.0")
// TEST FIXTURES
testFixturesApi(group = "io.kotest", name = "kotest-runner-junit5-jvm", version = kotestVersion)
@ -29,5 +49,13 @@ dependencies {
testFixturesApi(group = "io.ktor", name = "ktor-jackson", version = ktorVersion)
testFixturesApi(group = "io.ktor", name = "ktor-serialization", version = ktorVersion)
testFixturesApi(group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version = "1.3.1")
testFixturesApi(group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version = "1.3.2")
}
testing {
suites {
named("test", JvmTestSuite::class) {
useJUnitJupiter()
}
}
}

View File

@ -1,8 +1,5 @@
package io.bkbn.kompendium.core
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.SerializationFeature
import io.bkbn.kompendium.core.metadata.SchemaMap
import io.bkbn.kompendium.oas.OpenApiSpec
import io.bkbn.kompendium.oas.schema.TypedSchema
@ -12,7 +9,7 @@ import io.ktor.application.ApplicationFeature
import io.ktor.application.call
import io.ktor.http.HttpStatusCode
import io.ktor.request.path
import io.ktor.response.respondText
import io.ktor.response.respond
import io.ktor.util.AttributeKey
import kotlin.reflect.KClass
@ -28,13 +25,6 @@ class Kompendium(val config: Configuration) {
fun addCustomTypeSchema(clazz: KClass<*>, schema: TypedSchema) {
cache = cache.plus(clazz.simpleName!! to schema)
}
// TODO Add tests for this!!
var om: ObjectMapper = ObjectMapper()
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
.enable(SerializationFeature.INDENT_OUTPUT)
fun specToJson(): String = om.writeValueAsString(spec)
}
companion object Feature : ApplicationFeature<Application, Configuration, Kompendium> {
@ -44,8 +34,7 @@ class Kompendium(val config: Configuration) {
pipeline.intercept(ApplicationCallPipeline.Call) {
if (call.request.path() == configuration.specRoute) {
call.respondText { configuration.specToJson() }
call.response.status(HttpStatusCode.OK)
call.respond(HttpStatusCode.OK, configuration.spec)
}
}

View File

@ -199,8 +199,13 @@ object Kontent {
logger.debug("$slug contains $fieldMap")
var schema = ObjectSchema(fieldMap.plus(undeclaredFieldMap))
val requiredParams = clazz.primaryConstructor?.parameters?.filterNot { it.isOptional } ?: emptyList()
// todo de-dup this logic
if (requiredParams.isNotEmpty()) {
schema = schema.copy(required = requiredParams.map { it.name!! })
schema = schema.copy(required = requiredParams.map { param ->
clazz.memberProperties.first { it.name == param.name }.findAnnotation<Field>()
?.let { field -> field.name.ifBlank { param.name!! } }
?: param.name!!
})
}
logger.debug("$slug schema: $schema")
newCache.plus(slug to schema)
@ -335,8 +340,13 @@ object Kontent {
val requiredParams = clazz.primaryConstructor?.parameters?.filterNot { it.isOptional } ?: emptyList()
var schema = this
// todo dedup this
if (requiredParams.isNotEmpty()) {
schema = schema.copy(required = requiredParams.map { it.name!! })
schema = schema.copy(required = requiredParams.map { param ->
clazz.memberProperties.first { it.name == param.name }.findAnnotation<Field>()
?.let { field -> field.name.ifBlank { param.name!! } }
?: param.name!!
})
}
if (prop.returnType.isMarkedNullable) {

View File

@ -2,11 +2,15 @@ package io.bkbn.kompendium.core
import io.bkbn.kompendium.annotations.Param
import io.bkbn.kompendium.core.KompendiumPreFlight.methodNotarizationPreFlight
import io.bkbn.kompendium.core.MethodParser.parseMethodInfo
import io.bkbn.kompendium.core.metadata.method.DeleteInfo
import io.bkbn.kompendium.core.metadata.method.GetInfo
import io.bkbn.kompendium.core.metadata.method.HeadInfo
import io.bkbn.kompendium.core.metadata.method.OptionsInfo
import io.bkbn.kompendium.core.metadata.method.PatchInfo
import io.bkbn.kompendium.core.metadata.method.PostInfo
import io.bkbn.kompendium.core.metadata.method.PutInfo
import io.bkbn.kompendium.core.parser.DefaultMethodParser.calculateRoutePath
import io.bkbn.kompendium.core.parser.DefaultMethodParser.parseMethodInfo
import io.bkbn.kompendium.oas.path.Path
import io.bkbn.kompendium.oas.path.PathOperation
import io.ktor.application.ApplicationCall
@ -66,7 +70,7 @@ object Notarized {
}
/**
* Notarization for an HTTP Delete request
* Notarization for an HTTP PUT request
* @param TParam The class containing all parameter fields. Each field must be annotated with @[Param]
* @param TReq Class detailing the expected API request body
* @param TResp Class detailing the expected API response
@ -87,7 +91,28 @@ object Notarized {
}
/**
* Notarization for an HTTP POST request
* Notarization for an HTTP PATCH request
* @param TParam The class containing all parameter fields. Each field must be annotated with @[Param]
* @param TReq Class detailing the expected API request body
* @param TResp Class detailing the expected API response
* @param info Route metadata
* @param postProcess Adds an optional callback hook to perform manual overrides on the generated [PathOperation]
*/
inline fun <reified TParam : Any, reified TReq : Any, reified TResp : Any> Route.notarizedPatch(
info: PatchInfo<TParam, TReq, TResp>,
postProcess: (PathOperation) -> PathOperation = { p -> p },
noinline body: PipelineInterceptor<Unit, ApplicationCall>,
): Route = methodNotarizationPreFlight<TParam, TReq, TResp> { paramType, requestType, responseType ->
val feature = this.application.feature(Kompendium)
val path = calculateRoutePath()
feature.config.spec.paths.getOrPut(path) { Path() }
val baseInfo = parseMethodInfo(info, paramType, requestType, responseType, feature)
feature.config.spec.paths[path]?.patch = postProcess(baseInfo)
return method(HttpMethod.Patch) { handle(body) }
}
/**
* Notarization for an HTTP DELETE request
* @param TParam The class containing all parameter fields. Each field must be annotated with @[Param]
* @param TResp Class detailing the expected API response
* @param info Route metadata
@ -107,8 +132,41 @@ object Notarized {
}
/**
* Uses the built-in Ktor route path [Route.toString] but cuts out any meta route such as authentication... anything
* that matches the RegEx pattern `/\\(.+\\)`
* Notarization for an HTTP HEAD request
* @param TParam The class containing all parameter fields. Each field must be annotated with @[Param]
* @param info Route metadata
* @param postProcess Adds an optional callback hook to perform manual overrides on the generated [PathOperation]
*/
fun Route.calculateRoutePath() = toString().replace(Regex("/\\(.+\\)"), "")
inline fun <reified TParam : Any> Route.notarizedHead(
info: HeadInfo<TParam>,
postProcess: (PathOperation) -> PathOperation = { p -> p },
noinline body: PipelineInterceptor<Unit, ApplicationCall>
): Route = methodNotarizationPreFlight<TParam, Unit, Unit> { paramType, requestType, responseType ->
val feature = this.application.feature(Kompendium)
val path = calculateRoutePath()
feature.config.spec.paths.getOrPut(path) { Path() }
val baseInfo = parseMethodInfo(info, paramType, requestType, responseType, feature)
feature.config.spec.paths[path]?.head = postProcess(baseInfo)
return method(HttpMethod.Head) { handle(body) }
}
/**
* Notarization for an HTTP OPTION request
* @param TParam The class containing all parameter fields. Each field must be annotated with @[Param]
* @param TResp Class detailing the expected API response
* @param info Route metadata
* @param postProcess Adds an optional callback hook to perform manual overrides on the generated [PathOperation]
*/
inline fun <reified TParam : Any, reified TResp : Any> Route.notarizedOptions(
info: OptionsInfo<TParam, TResp>,
postProcess: (PathOperation) -> PathOperation = { p -> p },
noinline body: PipelineInterceptor<Unit, ApplicationCall>
): Route = methodNotarizationPreFlight<TParam, Unit, TResp> { paramType, requestType, responseType ->
val feature = this.application.feature(Kompendium)
val path = calculateRoutePath()
feature.config.spec.paths.getOrPut(path) { Path() }
val baseInfo = parseMethodInfo(info, paramType, requestType, responseType, feature)
feature.config.spec.paths[path]?.options = postProcess(baseInfo)
return method(HttpMethod.Options) { handle(body) }
}
}

View File

@ -0,0 +1,3 @@
package io.bkbn.kompendium.core.metadata
data class ParameterExample(val parameterName: String, val exampleName: String, val exampleValue: Any)

View File

@ -1,6 +1,7 @@
package io.bkbn.kompendium.core.metadata.method
import io.bkbn.kompendium.core.metadata.ExceptionInfo
import io.bkbn.kompendium.core.metadata.ParameterExample
import io.bkbn.kompendium.core.metadata.ResponseInfo
data class DeleteInfo<TParam, TResp>(
@ -11,6 +12,6 @@ data class DeleteInfo<TParam, TResp>(
override val deprecated: Boolean = false,
override val securitySchemes: Set<String> = emptySet(),
override val canThrow: Set<ExceptionInfo<*>> = emptySet(),
override val parameterExamples: Map<String, TParam> = emptyMap(),
override val parameterExamples: Set<ParameterExample> = emptySet(),
override val operationId: String? = null
) : MethodInfo<TParam, TResp>

View File

@ -1,6 +1,7 @@
package io.bkbn.kompendium.core.metadata.method
import io.bkbn.kompendium.core.metadata.ExceptionInfo
import io.bkbn.kompendium.core.metadata.ParameterExample
import io.bkbn.kompendium.core.metadata.ResponseInfo
data class GetInfo<TParam, TResp>(
@ -11,6 +12,6 @@ data class GetInfo<TParam, TResp>(
override val deprecated: Boolean = false,
override val securitySchemes: Set<String> = emptySet(),
override val canThrow: Set<ExceptionInfo<*>> = emptySet(),
override val parameterExamples: Map<String, TParam> = emptyMap(),
override val parameterExamples: Set<ParameterExample> = emptySet(),
override val operationId: String? = null
) : MethodInfo<TParam, TResp>

View File

@ -0,0 +1,17 @@
package io.bkbn.kompendium.core.metadata.method
import io.bkbn.kompendium.core.metadata.ExceptionInfo
import io.bkbn.kompendium.core.metadata.ParameterExample
import io.bkbn.kompendium.core.metadata.ResponseInfo
data class HeadInfo<TParam>(
override val responseInfo: ResponseInfo<Unit>,
override val summary: String,
override val description: String? = null,
override val tags: Set<String> = emptySet(),
override val deprecated: Boolean = false,
override val securitySchemes: Set<String> = emptySet(),
override val canThrow: Set<ExceptionInfo<*>> = emptySet(),
override val parameterExamples: Set<ParameterExample> = emptySet(),
override val operationId: String? = null
) : MethodInfo<TParam, Unit>

View File

@ -1,6 +1,7 @@
package io.bkbn.kompendium.core.metadata.method
import io.bkbn.kompendium.core.metadata.ExceptionInfo
import io.bkbn.kompendium.core.metadata.ParameterExample
import io.bkbn.kompendium.core.metadata.ResponseInfo
sealed interface MethodInfo<TParam, TResp> {
@ -16,9 +17,8 @@ sealed interface MethodInfo<TParam, TResp> {
val canThrow: Set<ExceptionInfo<*>>
get() = emptySet()
val responseInfo: ResponseInfo<TResp>
// TODO Is this even used anywhere?
val parameterExamples: Map<String, TParam>
get() = emptyMap()
val parameterExamples: Set<ParameterExample>
get() = emptySet()
val operationId: String?
get() = null
}

View File

@ -0,0 +1,17 @@
package io.bkbn.kompendium.core.metadata.method
import io.bkbn.kompendium.core.metadata.ExceptionInfo
import io.bkbn.kompendium.core.metadata.ParameterExample
import io.bkbn.kompendium.core.metadata.ResponseInfo
data class OptionsInfo<TParam, TResp>(
override val responseInfo: ResponseInfo<TResp>,
override val summary: String,
override val description: String? = null,
override val tags: Set<String> = emptySet(),
override val deprecated: Boolean = false,
override val securitySchemes: Set<String> = emptySet(),
override val canThrow: Set<ExceptionInfo<*>> = emptySet(),
override val parameterExamples: Set<ParameterExample> = emptySet(),
override val operationId: String? = null
) : MethodInfo<TParam, TResp>

View File

@ -0,0 +1,19 @@
package io.bkbn.kompendium.core.metadata.method
import io.bkbn.kompendium.core.metadata.ExceptionInfo
import io.bkbn.kompendium.core.metadata.ParameterExample
import io.bkbn.kompendium.core.metadata.RequestInfo
import io.bkbn.kompendium.core.metadata.ResponseInfo
data class PatchInfo<TParam, TReq, TResp>(
val requestInfo: RequestInfo<TReq>?,
override val responseInfo: ResponseInfo<TResp>,
override val summary: String,
override val description: String? = null,
override val tags: Set<String> = emptySet(),
override val deprecated: Boolean = false,
override val securitySchemes: Set<String> = emptySet(),
override val canThrow: Set<ExceptionInfo<*>> = emptySet(),
override val parameterExamples: Set<ParameterExample> = emptySet(),
override val operationId: String? = null
) : MethodInfo<TParam, TResp>

View File

@ -1,6 +1,7 @@
package io.bkbn.kompendium.core.metadata.method
import io.bkbn.kompendium.core.metadata.ExceptionInfo
import io.bkbn.kompendium.core.metadata.ParameterExample
import io.bkbn.kompendium.core.metadata.RequestInfo
import io.bkbn.kompendium.core.metadata.ResponseInfo
@ -13,6 +14,6 @@ data class PostInfo<TParam, TReq, TResp>(
override val deprecated: Boolean = false,
override val securitySchemes: Set<String> = emptySet(),
override val canThrow: Set<ExceptionInfo<*>> = emptySet(),
override val parameterExamples: Map<String, TParam> = emptyMap(),
override val parameterExamples: Set<ParameterExample> = emptySet(),
override val operationId: String? = null
) : MethodInfo<TParam, TResp>

View File

@ -1,6 +1,7 @@
package io.bkbn.kompendium.core.metadata.method
import io.bkbn.kompendium.core.metadata.ExceptionInfo
import io.bkbn.kompendium.core.metadata.ParameterExample
import io.bkbn.kompendium.core.metadata.RequestInfo
import io.bkbn.kompendium.core.metadata.ResponseInfo
@ -13,6 +14,6 @@ data class PutInfo<TParam, TReq, TResp>(
override val deprecated: Boolean = false,
override val securitySchemes: Set<String> = emptySet(),
override val canThrow: Set<ExceptionInfo<*>> = emptySet(),
override val parameterExamples: Map<String, TParam> = emptyMap(),
override val parameterExamples: Set<ParameterExample> = emptySet(),
override val operationId: String? = null
) : MethodInfo<TParam, TResp>

View File

@ -0,0 +1,3 @@
package io.bkbn.kompendium.core.parser
object DefaultMethodParser : IMethodParser

View File

@ -1,8 +1,10 @@
package io.bkbn.kompendium.core
package io.bkbn.kompendium.core.parser
import io.bkbn.kompendium.annotations.Param
import io.bkbn.kompendium.core.Kontent.generateKontent
import io.bkbn.kompendium.core.Kompendium
import io.bkbn.kompendium.core.Kontent
import io.bkbn.kompendium.core.metadata.ExceptionInfo
import io.bkbn.kompendium.core.metadata.ParameterExample
import io.bkbn.kompendium.core.metadata.RequestInfo
import io.bkbn.kompendium.core.metadata.ResponseInfo
import io.bkbn.kompendium.core.metadata.method.MethodInfo
@ -18,22 +20,20 @@ import io.bkbn.kompendium.oas.payload.Request
import io.bkbn.kompendium.oas.payload.Response
import io.bkbn.kompendium.oas.schema.AnyOfSchema
import io.bkbn.kompendium.oas.schema.ObjectSchema
import io.ktor.routing.Route
import kotlin.reflect.KClass
import kotlin.reflect.KParameter
import kotlin.reflect.KProperty
import kotlin.reflect.KType
import kotlin.reflect.full.createType
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.hasAnnotation
import kotlin.reflect.full.memberProperties
import kotlin.reflect.full.primaryConstructor
import java.util.Locale
import java.util.UUID
/**
* The MethodParser is responsible for converting route metadata and types into an OpenAPI compatible data class.
*/
object MethodParser {
interface IMethodParser {
/**
* Generates the OpenAPI Path spec from provided metadata
* @param info implementation of the [MethodInfo] sealed class
@ -54,7 +54,7 @@ object MethodParser {
operationId = info.operationId,
tags = info.tags,
deprecated = info.deprecated,
parameters = paramType.toParameterSpec(feature),
parameters = paramType.toParameterSpec(info, feature),
responses = parseResponse(responseType, info.responseInfo, feature).plus(parseExceptions(info.canThrow, feature)),
requestBody = when (info) {
is PutInfo<*, *, *> -> requestType.toRequestSpec(info.requestInfo, feature)
@ -67,17 +67,17 @@ object MethodParser {
) else null
)
private fun parseResponse(
fun parseResponse(
responseType: KType,
responseInfo: ResponseInfo<*>?,
feature: Kompendium
): Map<Int, Response<*>> = responseType.toResponseSpec(responseInfo, feature)?.let { mapOf(it) }.orEmpty()
): Map<Int, Response> = responseType.toResponseSpec(responseInfo, feature)?.let { mapOf(it) }.orEmpty()
private fun parseExceptions(
fun parseExceptions(
exceptionInfo: Set<ExceptionInfo<*>>,
feature: Kompendium,
): Map<Int, Response<*>> = exceptionInfo.associate { info ->
feature.config.cache = generateKontent(info.responseType, feature.config.cache)
): Map<Int, Response> = exceptionInfo.associate { info ->
feature.config.cache = Kontent.generateKontent(info.responseType, feature.config.cache)
val response = Response(
description = info.description,
content = feature.resolveContent(info.responseType, info.mediaTypes, info.examples)
@ -91,13 +91,14 @@ object MethodParser {
* @param requestInfo request metadata
* @return Will return a generated [Request] if requestInfo is not null
*/
private fun KType.toRequestSpec(requestInfo: RequestInfo<*>?, feature: Kompendium): Request<*>? =
fun KType.toRequestSpec(requestInfo: RequestInfo<*>?, feature: Kompendium): Request? =
when (requestInfo) {
null -> null
else -> {
Request(
description = requestInfo.description,
content = feature.resolveContent(this, requestInfo.mediaTypes, requestInfo.examples) ?: mapOf(),
content = feature.resolveContent(this, requestInfo.mediaTypes, requestInfo.examples as Map<String, Any>)
?: mapOf(),
required = requestInfo.required
)
}
@ -109,13 +110,13 @@ object MethodParser {
* @param responseInfo response metadata
* @return Will return a generated [Pair] if responseInfo is not null
*/
private fun KType.toResponseSpec(responseInfo: ResponseInfo<*>?, feature: Kompendium): Pair<Int, Response<*>>? =
fun KType.toResponseSpec(responseInfo: ResponseInfo<*>?, feature: Kompendium): Pair<Int, Response>? =
when (responseInfo) {
null -> null
else -> {
val specResponse = Response(
description = responseInfo.description,
content = feature.resolveContent(this, responseInfo.mediaTypes, responseInfo.examples)
content = feature.resolveContent(this, responseInfo.mediaTypes, responseInfo.examples as Map<String, Any>)
)
Pair(responseInfo.status.value, specResponse)
}
@ -128,11 +129,11 @@ object MethodParser {
* @param examples Mapping of named examples of valid bodies.
* @return Named mapping of media types.
*/
private fun <F> Kompendium.resolveContent(
fun Kompendium.resolveContent(
type: KType,
mediaTypes: List<String>,
examples: Map<String, F>
): Map<String, MediaType<F>>? {
examples: Map<String, Any>
): Map<String, MediaType>? {
val classifier = type.classifier as KClass<*>
return if (type != Helpers.UNIT_TYPE && mediaTypes.isNotEmpty()) {
mediaTypes.associateWith {
@ -161,24 +162,41 @@ object MethodParser {
* @return list of valid parameter specs as detailed by the [KType] members
* @throws [IllegalStateException] if the class could not be parsed properly
*/
private fun KType.toParameterSpec(feature: Kompendium): List<Parameter> {
fun KType.toParameterSpec(info: MethodInfo<*, *>, feature: Kompendium): List<Parameter> {
val clazz = classifier as KClass<*>
return clazz.memberProperties.filter { prop ->
prop.findAnnotation<Param>() != null
}.map { prop ->
val wrapperSchema = feature.config.cache[this.getSimpleSlug()]!! as ObjectSchema
val anny = prop.findAnnotation<Param>()
?: error("Field ${prop.name} is not annotated with KompendiumParam")
val schema = wrapperSchema.properties[prop.name]
?: error("Could not find component type for $prop")
val defaultValue = getDefaultParameterValue(clazz, prop)
Parameter(
name = prop.name,
`in` = anny.type.name.lowercase(Locale.getDefault()),
schema = schema.addDefault(defaultValue),
description = schema.description,
required = !prop.returnType.isMarkedNullable && defaultValue == null
)
return clazz.memberProperties
.filter { prop -> prop.hasAnnotation<Param>() }
.map { prop -> prop.toParameter(info, this, clazz, feature) }
}
fun KProperty<*>.toParameter(
info: MethodInfo<*, *>,
parentType: KType,
parentClazz: KClass<*>,
feature: Kompendium
): Parameter {
val wrapperSchema = feature.config.cache[parentType.getSimpleSlug()]!! as ObjectSchema
val anny = this.findAnnotation<Param>()
?: error("Field $name is not annotated with KompendiumParam")
val schema = wrapperSchema.properties[name]
?: error("Could not find component type for $this")
val defaultValue = getDefaultParameterValue(parentClazz, this)
return Parameter(
name = name,
`in` = anny.type.name.lowercase(Locale.getDefault()),
schema = schema.addDefault(defaultValue),
description = schema.description,
required = !returnType.isMarkedNullable && defaultValue == null,
examples = info.parameterExamples.mapToSpec(name)
)
}
fun Set<ParameterExample>.mapToSpec(parameterName: String): Map<String, Parameter.Example>? {
val filtered = filter { it.parameterName == parameterName }
return if (filtered.isEmpty()) {
null
} else {
filtered.associate { it.exampleName to Parameter.Example(it.exampleValue) }
}
}
@ -188,7 +206,7 @@ object MethodParser {
* @param prop the property in question
* @return The default value if found
*/
private fun getDefaultParameterValue(clazz: KClass<*>, prop: KProperty<*>): Any? {
fun getDefaultParameterValue(clazz: KClass<*>, prop: KProperty<*>): Any? {
val constructor = clazz.primaryConstructor
val parameterInQuestion = constructor
?.parameters
@ -215,7 +233,7 @@ object MethodParser {
* @return value of the proper type to match param
* @throws [IllegalStateException] if parameter type is not one of the basic types supported below.
*/
private fun defaultValueInjector(param: KParameter): Any = when (param.type.classifier) {
fun defaultValueInjector(param: KParameter): Any = when (param.type.classifier) {
String::class -> "test"
Boolean::class -> false
Int::class -> 1
@ -225,4 +243,10 @@ object MethodParser {
UUID::class -> UUID.randomUUID()
else -> error("Unsupported Type")
}
/**
* Uses the built-in Ktor route path [Route.toString] but cuts out any meta route such as authentication... anything
* that matches the RegEx pattern `/\\(.+\\)`
*/
fun Route.calculateRoutePath() = toString().replace(Regex("/\\(.+\\)"), "")
}

View File

@ -8,6 +8,7 @@ import io.bkbn.kompendium.core.util.constrainedDoubleInfo
import io.bkbn.kompendium.core.util.constrainedIntInfo
import io.bkbn.kompendium.core.util.defaultField
import io.bkbn.kompendium.core.util.defaultParameter
import io.bkbn.kompendium.core.util.exampleParams
import io.bkbn.kompendium.core.util.exclusiveMinMax
import io.bkbn.kompendium.core.util.formattedParam
import io.bkbn.kompendium.core.util.freeFormObject
@ -27,6 +28,9 @@ import io.bkbn.kompendium.core.util.notarizedGetWithGenericErrorResponse
import io.bkbn.kompendium.core.util.notarizedGetWithMultipleThrowables
import io.bkbn.kompendium.core.util.notarizedGetWithNotarizedException
import io.bkbn.kompendium.core.util.notarizedGetWithPolymorphicErrorResponse
import io.bkbn.kompendium.core.util.notarizedHeadModule
import io.bkbn.kompendium.core.util.notarizedOptionsModule
import io.bkbn.kompendium.core.util.notarizedPatchModule
import io.bkbn.kompendium.core.util.notarizedPostModule
import io.bkbn.kompendium.core.util.notarizedPutModule
import io.bkbn.kompendium.core.util.nullableField
@ -55,56 +59,53 @@ import io.ktor.http.HttpStatusCode
class KompendiumTest : DescribeSpec({
describe("Notarized Open API Metadata Tests") {
it("Can notarize a get request") {
// act
openApiTest("notarized_get.json") { notarizedGetModule() }
}
it("Can notarize a post request") {
// act
openApiTest("notarized_post.json") { notarizedPostModule() }
}
it("Can notarize a put request") {
// act
openApiTest("notarized_put.json") { notarizedPutModule() }
}
it("Can notarize a delete request") {
// act
openApiTest("notarized_delete.json") { notarizedDeleteModule() }
}
it("Can notarize a patch request") {
openApiTest("notarized_patch.json") { notarizedPatchModule() }
}
it("Can notarize a head request") {
openApiTest("notarized_head.json") { notarizedHeadModule() }
}
it("Can notarize an options request") {
openApiTest("notarized_options.json") { notarizedOptionsModule() }
}
it("Can notarize a complex type") {
// act
openApiTest("complex_type.json") { complexType() }
}
it("Can notarize primitives") {
// act
openApiTest("notarized_primitives.json") { primitives() }
}
it("Can notarize a top level list response") {
// act
openApiTest("response_list.json") { returnsList() }
}
it("Can notarize a route with non-required params") {
// act
openApiTest("non_required_params.json") { nonRequiredParamsGet() }
}
}
describe("Notarized Ktor Functionality Tests") {
it("Can notarized a get request and return the expected result") {
// act
apiFunctionalityTest("hey dude ‼️ congratz on the get request") { notarizedGetModule() }
}
it("Can notarize a post request and return the expected result") {
// act
apiFunctionalityTest(
"hey dude ✌️ congratz on the post request",
httpMethod = HttpMethod.Post
) { notarizedPostModule() }
}
it("Can notarize a put request and return the expected result") {
// act
apiFunctionalityTest("hey pal 🌝 whatcha doin' here?", httpMethod = HttpMethod.Put) { notarizedPutModule() }
}
it("Can notarize a delete request and return the expected result") {
// act
apiFunctionalityTest(
null,
httpMethod = HttpMethod.Delete,
@ -112,59 +113,50 @@ class KompendiumTest : DescribeSpec({
) { notarizedDeleteModule() }
}
it("Can notarize the root route and return the expected result") {
// act
apiFunctionalityTest("☎️🏠🌲", "/") { rootModule() }
}
it("Can notarize a trailing slash route and return the expected result") {
// act
apiFunctionalityTest("🙀👾", "/test/") { trailingSlash() }
}
}
describe("Route Parsing") {
it("Can parse a simple path and store it under the expected route") {
// act
openApiTest("path_parser.json") { pathParsingTestModule() }
}
it("Can notarize the root route") {
// act
openApiTest("root_route.json") { rootModule() }
}
it("Can notarize a route under the root module without appending trailing slash") {
// act
openApiTest("nested_under_root.json") { nestedUnderRootModule() }
}
it("Can notarize a route with a trailing slash") {
// act
openApiTest("trailing_slash.json") { trailingSlash() }
}
}
describe("Exceptions") {
it("Can add an exception status code to a response") {
// act
openApiTest("notarized_get_with_exception_response.json") { notarizedGetWithNotarizedException() }
}
it("Can support multiple response codes") {
// act
openApiTest("notarized_get_with_multiple_exception_responses.json") { notarizedGetWithMultipleThrowables() }
}
it("Can add a polymorphic exception response") {
// act
openApiTest("polymorphic_error_status_codes.json") { notarizedGetWithPolymorphicErrorResponse() }
}
it("Can add a generic exception response") {
// act
openApiTest("generic_exception.json") { notarizedGetWithGenericErrorResponse() }
}
}
describe("Examples") {
it("Can generate example response and request bodies") {
// act
openApiTest("example_req_and_resp.json") { withExamples() }
}
it("Can describe example parameters") {
openApiTest("example_parameters.json") { exampleParams() }
}
}
describe("Defaults") {
it("Can generate a default parameter values") {
// act
openApiTest("query_with_default_parameter.json") { withDefaultParameter() }
}
}
@ -184,49 +176,38 @@ class KompendiumTest : DescribeSpec({
}
describe("Polymorphism and Generics") {
it("can generate a polymorphic response type") {
// act
openApiTest("polymorphic_response.json") { polymorphicResponse() }
}
it("Can generate a collection with polymorphic response type") {
// act
openApiTest("polymorphic_list_response.json") { polymorphicCollectionResponse() }
}
it("Can generate a map with a polymorphic response type") {
// act
openApiTest("polymorphic_map_response.json") { polymorphicMapResponse() }
}
it("Can generate a polymorphic response from a sealed interface") {
// act
openApiTest("sealed_interface_response.json") { polymorphicInterfaceResponse() }
}
it("Can generate a response type with a generic type") {
// act
openApiTest("generic_response.json") { simpleGenericResponse() }
}
it("Can generate a polymorphic response type with generics") {
// act
openApiTest("polymorphic_response_with_generics.json") { genericPolymorphicResponse() }
}
it("Can handle an absolutely psycho inheritance test") {
// act
openApiTest("crazy_polymorphic_example.json") { genericPolymorphicResponseMultipleImpls() }
}
}
describe("Miscellaneous") {
it("Can generate the necessary ReDoc home page") {
// act
apiFunctionalityTest(getFileSnapshot("redoc.html"), "/docs") { returnsList() }
}
it("Can add an operation id to a notarized route") {
// act
openApiTest("notarized_get_with_operation_id.json") { withOperationId() }
}
it("Can add an undeclared field") {
// act
openApiTest("undeclared_field.json") { undeclaredType() }
}
it("Can add a custom header parameter with a name override") {
// act
openApiTest("override_parameter_name.json") { headerParameter() }
}
it("Can override field values via annotation") {

View File

@ -2,6 +2,9 @@ package io.bkbn.kompendium.core.util
import io.bkbn.kompendium.core.Notarized.notarizedDelete
import io.bkbn.kompendium.core.Notarized.notarizedGet
import io.bkbn.kompendium.core.Notarized.notarizedHead
import io.bkbn.kompendium.core.Notarized.notarizedOptions
import io.bkbn.kompendium.core.Notarized.notarizedPatch
import io.bkbn.kompendium.core.Notarized.notarizedPost
import io.bkbn.kompendium.core.Notarized.notarizedPut
import io.bkbn.kompendium.core.fixtures.Bibbity
@ -105,6 +108,36 @@ fun Application.notarizedDeleteModule() {
}
}
fun Application.notarizedPatchModule() {
routing {
route("/test") {
notarizedPatch(TestResponseInfo.testPatchInfo) {
call.respondText { "hey dude ✌️ congratz on the patch request" }
}
}
}
}
fun Application.notarizedHeadModule() {
routing {
route("/test") {
notarizedHead(TestResponseInfo.testHeadInfo) {
call.response.status(HttpStatusCode.OK)
}
}
}
}
fun Application.notarizedOptionsModule() {
routing {
route("/test") {
notarizedOptions(TestResponseInfo.testOptionsInfo) {
call.response.status(HttpStatusCode.OK)
}
}
}
}
fun Application.notarizedPutModule() {
routing {
route("/test") {
@ -245,12 +278,12 @@ fun Application.withDefaultParameter() {
}
}
fun Application.withOperationId(){
fun Application.withOperationId() {
routing {
route("/test") {
notarizedGet(
info = TestResponseInfo.testGetInfo.copy(operationId = "getTest")
){
) {
call.respond(HttpStatusCode.OK)
}
}
@ -531,3 +564,13 @@ fun Application.minMaxFreeForm() {
}
}
}
fun Application.exampleParams() {
routing {
route("/test/{a}") {
notarizedGet(TestResponseInfo.exampleParams) {
call.respondText { "Hi 🌊" }
}
}
}
}

View File

@ -80,7 +80,7 @@
},
"required": [
"org",
"amazingField",
"amazing_field",
"tables"
],
"type": "object"

View File

@ -0,0 +1,97 @@
{
"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/{a}": {
"get": {
"tags": [],
"summary": "param stuff",
"description": "Cool stuff",
"parameters": [
{
"name": "a",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false,
"examples": {
"Testerino": {
"value": "a"
},
"Testerina": {
"value": "b"
}
}
},
{
"name": "aa",
"in": "query",
"schema": {
"format": "int32",
"type": "integer"
},
"required": true,
"deprecated": false,
"examples": {
"Wowza": {
"value": 6
}
}
}
],
"responses": {
"200": {
"description": "A successful endeavor",
"content": {
"application/json": {
"schema": {
"properties": {
"c": {
"type": "string"
}
},
"required": [
"c"
],
"type": "object"
}
}
}
}
},
"deprecated": false
}
}
},
"components": {
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -62,7 +62,7 @@
}
},
"required": [
"fieldName",
"field_name",
"b",
"aaa"
],

View File

@ -45,7 +45,7 @@
}
},
"required": [
"b"
"real_name"
],
"type": "object"
}

View File

@ -0,0 +1,49 @@
{
"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": {
"head": {
"tags": [],
"summary": "Test head endpoint",
"description": "head test 💀",
"parameters": [],
"responses": {
"200": {
"description": "great!"
}
},
"deprecated": false
}
}
},
"components": {
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -0,0 +1,84 @@
{
"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": {
"options": {
"tags": [],
"summary": "Test options",
"description": "endpoint of options",
"parameters": [
{
"name": "a",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
},
{
"name": "aa",
"in": "query",
"schema": {
"format": "int32",
"type": "integer"
},
"required": true,
"deprecated": false
}
],
"responses": {
"200": {
"description": "nice",
"content": {
"application/json": {
"schema": {
"properties": {
"c": {
"type": "string"
}
},
"required": [
"c"
],
"type": "object"
}
}
}
}
},
"deprecated": false
}
}
},
"components": {
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -0,0 +1,64 @@
{
"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": {
"patch": {
"tags": [],
"summary": "Test patch endpoint",
"description": "patch your tests here!",
"parameters": [],
"responses": {
"201": {
"description": "A Successful Endeavor",
"content": {
"application/json": {
"schema": {
"properties": {
"c": {
"type": "string"
}
},
"required": [
"c"
],
"type": "object"
}
}
}
}
},
"deprecated": false
}
}
},
"components": {
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -82,7 +82,7 @@
}
},
"required": [
"fieldName",
"field_name",
"b",
"aaa"
],

View File

@ -82,7 +82,7 @@
}
},
"required": [
"fieldName",
"field_name",
"b",
"aaa"
],

View File

@ -1,12 +1,16 @@
package io.bkbn.kompendium.core.fixtures
import io.bkbn.kompendium.core.metadata.ExceptionInfo
import io.bkbn.kompendium.core.metadata.ParameterExample
import io.bkbn.kompendium.core.metadata.method.PostInfo
import io.bkbn.kompendium.core.metadata.method.PutInfo
import io.bkbn.kompendium.core.metadata.RequestInfo
import io.bkbn.kompendium.core.metadata.ResponseInfo
import io.bkbn.kompendium.core.metadata.method.DeleteInfo
import io.bkbn.kompendium.core.metadata.method.GetInfo
import io.bkbn.kompendium.core.metadata.method.HeadInfo
import io.bkbn.kompendium.core.metadata.method.OptionsInfo
import io.bkbn.kompendium.core.metadata.method.PatchInfo
import io.ktor.http.HttpStatusCode
import kotlin.reflect.typeOf
@ -15,6 +19,7 @@ object TestResponseInfo {
private val testGetListResponse =
ResponseInfo<List<TestResponse>>(HttpStatusCode.OK, "A Successful List-y Endeavor")
private val testPostResponse = ResponseInfo<TestCreatedResponse>(HttpStatusCode.Created, "A Successful Endeavor")
private val testPatchResponse = ResponseInfo<TestResponse>(HttpStatusCode.Created, "A Successful Endeavor")
private val testPostResponseAgain = ResponseInfo<Boolean>(HttpStatusCode.Created, "A Successful Endeavor")
private val testDeleteResponse =
ResponseInfo<Unit>(HttpStatusCode.NoContent, "A Successful Endeavor", mediaTypes = emptyList())
@ -75,6 +80,22 @@ object TestResponseInfo {
responseInfo = testPostResponse,
requestInfo = complexRequest
)
val testPatchInfo = PatchInfo<Unit, TestRequest, TestResponse>(
summary = "Test patch endpoint",
description = "patch your tests here!",
responseInfo = testPatchResponse,
requestInfo = testRequest
)
val testHeadInfo = HeadInfo<Unit>(
summary = "Test head endpoint",
description = "head test 💀",
responseInfo = ResponseInfo(HttpStatusCode.OK, "great!")
)
val testOptionsInfo = OptionsInfo<TestParams, TestResponse>(
summary = "Test options",
description = "endpoint of options",
responseInfo = ResponseInfo(HttpStatusCode.OK, "nice")
)
val testPutInfoAlso = PutInfo<TestParams, TestRequest, TestCreatedResponse>(
summary = "Test put endpoint",
description = "Put your tests here!",
@ -246,5 +267,16 @@ object TestResponseInfo {
requestInfo = RequestInfo("cool")
)
val exampleParams = GetInfo<TestParams, TestResponse>(
summary = "param stuff",
description = "Cool stuff",
responseInfo = simpleOkResponse(),
parameterExamples = setOf(
ParameterExample(TestParams::a.name, "Testerino", "a"),
ParameterExample(TestParams::a.name, "Testerina", "b"),
ParameterExample(TestParams::aa.name, "Wowza", 6),
)
)
private fun <T> simpleOkResponse() = ResponseInfo<T>(HttpStatusCode.OK, "A successful endeavor")
}

View File

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

View File

@ -1,5 +1,25 @@
plugins {
id("io.bkbn.sourdough.library")
kotlin("jvm")
id("io.bkbn.sourdough.library.jvm") version "0.5.4"
id("io.gitlab.arturbosch.detekt") version "1.19.0"
id("com.adarshr.test-logger") version "3.1.0"
id("org.jetbrains.dokka")
id("maven-publish")
id("java-library")
id("signing")
}
sourdough {
githubOrg.set("bkbnio")
githubRepo.set("kompendium")
libraryName.set("Kompendium Locations")
libraryDescription.set("Supplemental library for Kompendium offering support for Ktor's Location API")
licenseName.set("MIT License")
licenseUrl.set("https://mit-license.org")
developerId.set("unredundant")
developerName.set("Ryan Brink")
developerEmail.set("admin@bkbn.io")
compilerArgs.set(listOf("-opt-in=kotlin.RequiresOptIn"))
}
dependencies {
@ -14,3 +34,11 @@ dependencies {
testImplementation(testFixtures(projects.kompendiumCore))
}
testing {
suites {
named("test", JvmTestSuite::class) {
useJUnitJupiter()
}
}
}

View File

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

View File

@ -1,28 +1,19 @@
package io.bkbn.kompendium.locations
import io.bkbn.kompendium.core.Kompendium
import io.bkbn.kompendium.core.KompendiumPreFlight.methodNotarizationPreFlight
import io.bkbn.kompendium.core.MethodParser.parseMethodInfo
import io.bkbn.kompendium.core.Notarized.calculateRoutePath
import io.bkbn.kompendium.core.metadata.method.DeleteInfo
import io.bkbn.kompendium.core.metadata.method.GetInfo
import io.bkbn.kompendium.core.metadata.method.PostInfo
import io.bkbn.kompendium.core.metadata.method.PutInfo
import io.bkbn.kompendium.oas.path.Path
import io.bkbn.kompendium.oas.path.PathOperation
import io.ktor.application.ApplicationCall
import io.ktor.application.feature
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.application
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
@ -45,15 +36,8 @@ object NotarizedLocation {
postProcess: (PathOperation) -> PathOperation = { p -> p },
noinline body: suspend PipelineContext<Unit, ApplicationCall>.(TParam) -> Unit
): Route = 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 feature = application.feature(Kompendium)
val path = calculateRoutePath()
val locationPath = TParam::class.calculateLocationPath()
val pathWithLocation = path.plus(locationPath)
feature.config.spec.paths.getOrPut(pathWithLocation) { Path() }
val baseInfo = parseMethodInfo(info, paramType, requestType, responseType, feature)
feature.config.spec.paths[pathWithLocation]?.get = postProcess(baseInfo)
val lbi = LocationMethodParser.processBaseInfo<TParam>(paramType, requestType, responseType, info, this)
lbi.feature.config.spec.paths[lbi.path]?.get = postProcess(lbi.op)
return location(TParam::class) {
method(HttpMethod.Get) { handle(body) }
}
@ -74,15 +58,8 @@ object NotarizedLocation {
postProcess: (PathOperation) -> PathOperation = { p -> p },
noinline body: suspend PipelineContext<Unit, ApplicationCall>.(TParam) -> Unit
): Route = 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 feature = application.feature(Kompendium)
val path = calculateRoutePath()
val locationPath = TParam::class.calculateLocationPath()
val pathWithLocation = path.plus(locationPath)
feature.config.spec.paths.getOrPut(pathWithLocation) { Path() }
val baseInfo = parseMethodInfo(info, paramType, requestType, responseType, feature)
feature.config.spec.paths[pathWithLocation]?.post = postProcess(baseInfo)
val lbi = LocationMethodParser.processBaseInfo<TParam>(paramType, requestType, responseType, info, this)
lbi.feature.config.spec.paths[lbi.path]?.post = postProcess(lbi.op)
return location(TParam::class) {
method(HttpMethod.Post) { handle(body) }
}
@ -103,15 +80,8 @@ object NotarizedLocation {
postProcess: (PathOperation) -> PathOperation = { p -> p },
noinline body: suspend PipelineContext<Unit, ApplicationCall>.(TParam) -> Unit
): Route = 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 feature = application.feature(Kompendium)
val path = calculateRoutePath()
val locationPath = TParam::class.calculateLocationPath()
val pathWithLocation = path.plus(locationPath)
feature.config.spec.paths.getOrPut(pathWithLocation) { Path() }
val baseInfo = parseMethodInfo(info, paramType, requestType, responseType, feature)
feature.config.spec.paths[pathWithLocation]?.put = postProcess(baseInfo)
val lbi = LocationMethodParser.processBaseInfo<TParam>(paramType, requestType, responseType, info, this)
lbi.feature.config.spec.paths[lbi.path]?.put = postProcess(lbi.op)
return location(TParam::class) {
method(HttpMethod.Put) { handle(body) }
}
@ -131,28 +101,10 @@ object NotarizedLocation {
postProcess: (PathOperation) -> PathOperation = { p -> p },
noinline body: suspend PipelineContext<Unit, ApplicationCall>.(TParam) -> Unit
): Route = 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 feature = application.feature(Kompendium)
val path = calculateRoutePath()
val locationPath = TParam::class.calculateLocationPath()
val pathWithLocation = path.plus(locationPath)
feature.config.spec.paths.getOrPut(pathWithLocation) { Path() }
val baseInfo = parseMethodInfo(info, paramType, requestType, responseType, feature)
feature.config.spec.paths[pathWithLocation]?.delete = postProcess(baseInfo)
val lbi = LocationMethodParser.processBaseInfo<TParam>(paramType, requestType, responseType, info, this)
lbi.feature.config.spec.paths[lbi.path]?.delete = postProcess(lbi.op)
return location(TParam::class) {
method(HttpMethod.Delete) { handle(body) }
}
}
fun KClass<*>.calculateLocationPath(suffix: String = ""): String {
val locationAnnotation = this.findAnnotation<Location>()
require(locationAnnotation != null) { "Location annotation must be present to leverage notarized location api" }
val parent = this.java.declaringClass?.kotlin
val newSuffix = locationAnnotation.path.plus(suffix)
return when (parent) {
null -> newSuffix
else -> parent.calculateLocationPath(newSuffix)
}
}
}

View File

@ -40,6 +40,15 @@
},
"required": true,
"deprecated": false
},
{
"name": "name",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
}
],
"responses": {

View File

@ -40,6 +40,15 @@
},
"required": true,
"deprecated": false
},
{
"name": "name",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
}
],
"responses": {

View File

@ -40,6 +40,15 @@
},
"required": true,
"deprecated": false
},
{
"name": "name",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
}
],
"requestBody": {

View File

@ -40,6 +40,15 @@
},
"required": true,
"deprecated": false
},
{
"name": "name",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
}
],
"requestBody": {

View File

@ -1,3 +1,36 @@
plugins {
id("io.bkbn.sourdough.library")
kotlin("jvm")
kotlin("plugin.serialization") version "1.6.10"
id("io.bkbn.sourdough.library.jvm") version "0.5.4"
id("io.gitlab.arturbosch.detekt") version "1.19.0"
id("com.adarshr.test-logger") version "3.1.0"
id("org.jetbrains.dokka")
id("maven-publish")
id("java-library")
id("signing")
}
sourdough {
githubOrg.set("bkbnio")
githubRepo.set("kompendium")
libraryName.set("Kompendium OpenAPI Spec")
libraryDescription.set("Collections of kotlin data classes modeling the OpenAPI specification")
licenseName.set("MIT License")
licenseUrl.set("https://mit-license.org")
developerId.set("unredundant")
developerName.set("Ryan Brink")
developerEmail.set("admin@bkbn.io")
compilerArgs.set(listOf("-opt-in=kotlin.RequiresOptIn"))
}
dependencies {
implementation(group = "org.jetbrains.kotlinx", "kotlinx-serialization-json", version = "1.3.1")
}
testing {
suites {
named("test", JvmTestSuite::class) {
useJUnitJupiter()
}
}
}

View File

@ -6,7 +6,9 @@ import io.bkbn.kompendium.oas.component.Components
import io.bkbn.kompendium.oas.info.Info
import io.bkbn.kompendium.oas.path.Path
import io.bkbn.kompendium.oas.server.Server
import kotlinx.serialization.Serializable
@Serializable
data class OpenApiSpec(
val openapi: String = "3.0.3",
val info: Info,

View File

@ -1,8 +1,12 @@
package io.bkbn.kompendium.oas.common
import io.bkbn.kompendium.oas.serialization.UriSerializer
import kotlinx.serialization.Serializable
import java.net.URI
@Serializable
data class ExternalDocumentation(
@Serializable(with = UriSerializer::class)
val url: URI,
val description: String?
)

View File

@ -1,5 +1,8 @@
package io.bkbn.kompendium.oas.common
import kotlinx.serialization.Serializable
@Serializable
data class Tag(
val name: String,
val description: String? = null,

View File

@ -1,7 +1,9 @@
package io.bkbn.kompendium.oas.component
import io.bkbn.kompendium.oas.security.SecuritySchema
import kotlinx.serialization.Serializable
@Serializable
data class Components(
val securitySchemes: MutableMap<String, SecuritySchema> = mutableMapOf()
)

View File

@ -1,9 +1,13 @@
package io.bkbn.kompendium.oas.info
import io.bkbn.kompendium.oas.serialization.UriSerializer
import kotlinx.serialization.Serializable
import java.net.URI
@Serializable
data class Contact(
var name: String,
@Serializable(with = UriSerializer::class)
var url: URI? = null,
var email: String? = null // TODO Enforce email?
)

View File

@ -1,11 +1,15 @@
package io.bkbn.kompendium.oas.info
import io.bkbn.kompendium.oas.serialization.UriSerializer
import kotlinx.serialization.Serializable
import java.net.URI
@Serializable
data class Info(
var title: String? = null,
var version: String? = null,
var description: String? = null,
@Serializable(with = UriSerializer::class)
var termsOfService: URI? = null,
var contact: Contact? = null,
var license: License? = null

View File

@ -1,8 +1,12 @@
package io.bkbn.kompendium.oas.info
import io.bkbn.kompendium.oas.serialization.UriSerializer
import kotlinx.serialization.Serializable
import java.net.URI
@Serializable
data class License(
var name: String,
@Serializable(with = UriSerializer::class)
var url: URI? = null
)

View File

@ -2,7 +2,9 @@ package io.bkbn.kompendium.oas.path
import io.bkbn.kompendium.oas.payload.Parameter
import io.bkbn.kompendium.oas.server.Server
import kotlinx.serialization.Serializable
@Serializable
data class Path(
var get: PathOperation? = null,
var put: PathOperation? = null,

View File

@ -6,7 +6,9 @@ import io.bkbn.kompendium.oas.payload.Payload
import io.bkbn.kompendium.oas.payload.Request
import io.bkbn.kompendium.oas.payload.Response
import io.bkbn.kompendium.oas.server.Server
import kotlinx.serialization.Serializable
@Serializable
data class PathOperation(
var tags: Set<String> = emptySet(),
var summary: String? = null,
@ -14,9 +16,9 @@ data class PathOperation(
var externalDocs: ExternalDocumentation? = null,
var operationId: String? = null,
var parameters: List<Parameter>? = null,
var requestBody: Request<*>? = null,
var requestBody: Request? = null,
// TODO How to enforce `default` requirement 🧐
var responses: Map<Int, Response<*>>? = null,
var responses: Map<Int, Response>? = null,
var callbacks: Map<String, Payload>? = null, // todo what is this?
var deprecated: Boolean = false,
var security: List<Map<String, List<String>>>? = null,

View File

@ -1,10 +1,14 @@
package io.bkbn.kompendium.oas.payload
import io.bkbn.kompendium.oas.schema.ComponentSchema
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
data class MediaType<T>(
@Serializable
data class MediaType(
val schema: ComponentSchema,
val examples: Map<String, Example<T>>? = null
val examples: Map<String, Example>? = null
) {
data class Example<T>(val value: T)
@Serializable
data class Example(val value: @Contextual Any)
}

View File

@ -1,7 +1,10 @@
package io.bkbn.kompendium.oas.payload
import io.bkbn.kompendium.oas.schema.ComponentSchema
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
@Serializable
data class Parameter(
val name: String,
val `in`: String, // TODO Enum? "query", "header", "path" or "cookie"
@ -11,5 +14,9 @@ data class Parameter(
val deprecated: Boolean = false,
val allowEmptyValue: Boolean? = null,
val style: String? = null,
val explode: Boolean? = null
)
val explode: Boolean? = null,
val examples: Map<String, Example>? = null
) {
@Serializable
data class Example(val value: @Contextual Any)
}

View File

@ -1,7 +1,10 @@
package io.bkbn.kompendium.oas.payload
data class Request<T>(
import kotlinx.serialization.Serializable
@Serializable
data class Request(
val description: String?,
val content: Map<String, MediaType<T>>,
val content: Map<String, MediaType>,
val required: Boolean = false
) : Payload

View File

@ -1,8 +1,11 @@
package io.bkbn.kompendium.oas.payload
data class Response<T>(
import kotlinx.serialization.Serializable
@Serializable
data class Response(
val description: String? = null,
val headers: Map<String, Payload>? = null,
val content: Map<String, MediaType<T>>? = null,
val content: Map<String, MediaType>? = null,
val links: Map<String, Payload>? = null
) : Payload

View File

@ -1,3 +1,6 @@
package io.bkbn.kompendium.oas.schema
import kotlinx.serialization.Serializable
@Serializable
data class AnyOfSchema(val anyOf: List<ComponentSchema>, override val description: String? = null) : ComponentSchema

View File

@ -1,8 +1,12 @@
package io.bkbn.kompendium.oas.schema
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
@Serializable
data class ArraySchema(
val items: ComponentSchema,
override val default: Any? = null,
override val default: @Contextual Any? = null,
override val description: String? = null,
override val nullable: Boolean? = null,
// constraints

View File

@ -1,5 +1,10 @@
package io.bkbn.kompendium.oas.schema
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.JsonClassDiscriminator
@OptIn(ExperimentalSerializationApi::class)
@JsonClassDiscriminator("component_type") // todo figure out a way to filter this
sealed interface ComponentSchema {
val description: String?
get() = null

View File

@ -1,8 +1,12 @@
package io.bkbn.kompendium.oas.schema
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
@Serializable
data class DictionarySchema(
val additionalProperties: ComponentSchema,
override val default: Any? = null,
override val default: @Contextual Any? = null,
override val description: String? = null,
override val nullable: Boolean? = null
) : TypedSchema {

View File

@ -1,8 +1,12 @@
package io.bkbn.kompendium.oas.schema
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
@Serializable
data class EnumSchema(
val `enum`: Set<String>,
override val default: Any? = null,
override val default: @Contextual Any? = null,
override val description: String? = null,
override val nullable: Boolean? = null
) : TypedSchema {

View File

@ -1,15 +1,23 @@
package io.bkbn.kompendium.oas.schema
import io.bkbn.kompendium.oas.serialization.NumberSerializer
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
@Serializable
data class FormattedSchema(
val format: String,
override val type: String,
override val default: Any? = null,
override val default: @Contextual Any? = null,
override val description: String? = null,
override val nullable: Boolean? = null,
// Constraints
@Serializable(with = NumberSerializer::class)
val minimum: Number? = null,
@Serializable(with = NumberSerializer::class)
val maximum: Number? = null,
val exclusiveMinimum: Boolean? = null,
val exclusiveMaximum: Boolean? = null,
@Serializable(with = NumberSerializer::class)
val multipleOf: Number? = null,
) : TypedSchema

View File

@ -1,5 +1,9 @@
package io.bkbn.kompendium.oas.schema
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
@Serializable
data class FreeFormSchema(
override val nullable: Boolean? = null,
// constraints
@ -8,5 +12,5 @@ data class FreeFormSchema(
) : TypedSchema {
val additionalProperties: Boolean = true
override val type: String = "object"
override val default: Any? = null
override val default: @Contextual Any? = null
}

View File

@ -1,8 +1,12 @@
package io.bkbn.kompendium.oas.schema
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
@Serializable
data class ObjectSchema(
val properties: Map<String, ComponentSchema>,
override val default: Any? = null,
override val default: @Contextual Any? = null,
override val description: String? = null,
override val nullable: Boolean? = null,
// constraints

View File

@ -1,8 +1,12 @@
package io.bkbn.kompendium.oas.schema
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
@Serializable
data class SimpleSchema(
override val type: String,
override val default: Any? = null,
override val default: @Contextual Any? = null,
override val description: String? = null,
override val nullable: Boolean? = null,
// Constraints

View File

@ -1,11 +1,13 @@
package io.bkbn.kompendium.oas.security
import kotlinx.serialization.Serializable
import java.util.Locale
// TODO... is there even an official ktor api auth mechanism??
@Serializable
@Suppress("UnusedPrivateMember")
class ApiKeyAuth(val `in`: ApiKeyLocation, name: String) : SecuritySchema {
class ApiKeyAuth(val `in`: ApiKeyLocation, val name: String) : SecuritySchema {
val type: String = "apiKey"
enum class ApiKeyLocation {

View File

@ -1,5 +1,8 @@
package io.bkbn.kompendium.oas.security
import kotlinx.serialization.Serializable
@Serializable
class BasicAuth : SecuritySchema {
val type: String = "http"
val scheme: String = "basic"

View File

@ -1,5 +1,8 @@
package io.bkbn.kompendium.oas.security
import kotlinx.serialization.Serializable
@Serializable
data class BearerAuth(val bearerFormat: String? = null): SecuritySchema {
val type: String = "http"
val scheme: String = "bearer"

View File

@ -1,8 +1,12 @@
package io.bkbn.kompendium.oas.security
import kotlinx.serialization.Serializable
@Serializable
data class OAuth(val description: String? = null, val flows: Flows) : SecuritySchema {
val type: String = "oauth2"
@Serializable
data class Flows(
val implicit: Implicit? = null,
val authorizationCode: AuthorizationCode? = null,
@ -21,12 +25,14 @@ data class OAuth(val description: String? = null, val flows: Flows) : SecuritySc
get() = emptyMap()
}
@Serializable
data class Implicit(
override val authorizationUrl: String,
override val refreshUrl: String? = null,
override val scopes: Map<String, String> = emptyMap()
) : Flow
@Serializable
data class AuthorizationCode(
override val authorizationUrl: String,
override val tokenUrl: String? = null,
@ -34,12 +40,14 @@ data class OAuth(val description: String? = null, val flows: Flows) : SecuritySc
override val scopes: Map<String, String> = emptyMap()
) : Flow
@Serializable
data class Password(
override val tokenUrl: String? = null,
override val refreshUrl: String? = null,
override val scopes: Map<String, String> = emptyMap()
) : Flow
@Serializable
data class ClientCredential(
override val tokenUrl: String? = null,
override val refreshUrl: String? = null,

View File

@ -1,3 +1,8 @@
package io.bkbn.kompendium.oas.security
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.JsonClassDiscriminator
@OptIn(ExperimentalSerializationApi::class)
@JsonClassDiscriminator("schema_type") // todo figure out a way to filter this
sealed interface SecuritySchema

View File

@ -0,0 +1,27 @@
package io.bkbn.kompendium.oas.serialization
import kotlin.reflect.KClass
import kotlinx.serialization.InternalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.serializer
class AnySerializer<T : Any> : KSerializer<T> {
override fun serialize(encoder: Encoder, value: T) {
serialize(encoder, value, value::class as KClass<T>)
}
override fun deserialize(decoder: Decoder): T {
error("Abandon all hope ye who enter 💀")
}
override val descriptor: SerialDescriptor
get() = TODO("Not yet implemented")
@OptIn(InternalSerializationApi::class)
fun serialize(encoder: Encoder, obj: T, clazz: KClass<T>) {
clazz.serializer().serialize(encoder, obj)
}
}

View File

@ -0,0 +1,42 @@
package io.bkbn.kompendium.oas.serialization
import io.bkbn.kompendium.oas.schema.AnyOfSchema
import io.bkbn.kompendium.oas.schema.ArraySchema
import io.bkbn.kompendium.oas.schema.ComponentSchema
import io.bkbn.kompendium.oas.schema.DictionarySchema
import io.bkbn.kompendium.oas.schema.EnumSchema
import io.bkbn.kompendium.oas.schema.FormattedSchema
import io.bkbn.kompendium.oas.schema.FreeFormSchema
import io.bkbn.kompendium.oas.schema.ObjectSchema
import io.bkbn.kompendium.oas.schema.SimpleSchema
import io.bkbn.kompendium.oas.security.ApiKeyAuth
import io.bkbn.kompendium.oas.security.BasicAuth
import io.bkbn.kompendium.oas.security.BearerAuth
import io.bkbn.kompendium.oas.security.OAuth
import io.bkbn.kompendium.oas.security.SecuritySchema
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.polymorphic
object KompendiumSerializersModule {
val module = SerializersModule {
polymorphic(ComponentSchema::class) {
subclass(SimpleSchema::class, SimpleSchema.serializer())
subclass(FormattedSchema::class, FormattedSchema.serializer())
subclass(ObjectSchema::class, ObjectSchema.serializer())
subclass(AnyOfSchema::class, AnyOfSchema.serializer())
subclass(ArraySchema::class, ArraySchema.serializer())
subclass(DictionarySchema::class, DictionarySchema.serializer())
subclass(EnumSchema::class, EnumSchema.serializer())
subclass(FreeFormSchema::class, FreeFormSchema.serializer())
}
polymorphic(SecuritySchema::class) {
subclass(ApiKeyAuth::class, ApiKeyAuth.serializer())
subclass(BasicAuth::class, BasicAuth.serializer())
subclass(BearerAuth::class, BearerAuth.serializer())
subclass(OAuth::class, OAuth.serializer())
}
contextual(Any::class, AnySerializer())
}
}

View File

@ -0,0 +1,23 @@
package io.bkbn.kompendium.oas.serialization
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerializationException
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
object NumberSerializer : KSerializer<Number> {
override fun deserialize(decoder: Decoder): Number = try {
decoder.decodeDouble()
} catch (_: SerializationException) {
decoder.decodeInt()
}
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Number", PrimitiveKind.DOUBLE)
override fun serialize(encoder: Encoder, value: Number) {
encoder.encodeString(value.toString())
}
}

View File

@ -0,0 +1,20 @@
package io.bkbn.kompendium.oas.serialization
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import java.net.URI
object UriSerializer : KSerializer<URI> {
override fun deserialize(decoder: Decoder): URI = URI.create(decoder.decodeString())
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("URI", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: URI) {
encoder.encodeString(value.toString())
}
}

View File

@ -1,8 +1,12 @@
package io.bkbn.kompendium.oas.server
import io.bkbn.kompendium.oas.serialization.UriSerializer
import kotlinx.serialization.Serializable
import java.net.URI
@Serializable
data class Server(
@Serializable(with = UriSerializer::class)
val url: URI,
val description: String? = null,
var variables: Map<String, ServerVariable>? = null

View File

@ -1,5 +1,8 @@
package io.bkbn.kompendium.oas.server
import kotlinx.serialization.Serializable
@Serializable
data class ServerVariable(
val `enum`: Set<String>, // todo enforce not empty
val default: String,

View File

@ -1,6 +1,12 @@
plugins {
kotlin("plugin.serialization") version "1.6.0"
application
kotlin("jvm")
kotlin("plugin.serialization") version "1.6.10"
id("io.bkbn.sourdough.application.jvm") version "0.5.4"
id("application")
}
sourdough {
compilerArgs.set(listOf("-opt-in=kotlin.RequiresOptIn"))
}
dependencies {
@ -18,10 +24,23 @@ dependencies {
implementation(group = "io.ktor", name = "ktor-auth", version = ktorVersion)
implementation(group = "io.ktor", name = "ktor-auth-jwt", version = ktorVersion)
implementation(group = "io.ktor", name = "ktor-serialization", version = ktorVersion)
implementation(group = "io.ktor", name = "ktor-jackson", version = ktorVersion)
implementation(group = "io.ktor", name = "ktor-gson", version = ktorVersion)
implementation(group = "io.ktor", name = "ktor-locations", version = ktorVersion)
implementation(group = "io.ktor", name = "ktor-webjars", version = ktorVersion)
// Logging
implementation("org.apache.logging.log4j:log4j-api-kotlin:1.1.0")
implementation("org.apache.logging.log4j:log4j-api:2.17.1")
implementation("org.apache.logging.log4j:log4j-core:2.17.1")
implementation("org.slf4j:slf4j-api:1.7.32")
implementation("org.slf4j:slf4j-simple:1.7.32")
implementation(group = "org.jetbrains.kotlinx", "kotlinx-serialization-json", version = "1.3.1")
implementation(group = "joda-time", name = "joda-time", version = "2.10.13")
}
repositories {
mavenCentral()
}

View File

@ -7,12 +7,8 @@ import io.bkbn.kompendium.core.Notarized.notarizedGet
import io.bkbn.kompendium.core.metadata.ResponseInfo
import io.bkbn.kompendium.core.metadata.method.GetInfo
import io.bkbn.kompendium.core.routes.redoc
import io.bkbn.kompendium.oas.OpenApiSpec
import io.bkbn.kompendium.oas.info.Contact
import io.bkbn.kompendium.oas.info.Info
import io.bkbn.kompendium.oas.info.License
import io.bkbn.kompendium.oas.server.Server
import io.bkbn.kompendium.playground.AuthPlaygroundToC.simpleAuthenticatedGet
import io.bkbn.kompendium.playground.util.Util
import io.ktor.application.Application
import io.ktor.application.call
import io.ktor.application.install
@ -27,7 +23,6 @@ import io.ktor.serialization.json
import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty
import kotlinx.serialization.Serializable
import java.net.URI
/**
* Application entrypoint. Run this and head on over to `localhost:8081/docs`
@ -44,10 +39,10 @@ fun main() {
// Application Module
private fun Application.mainModule() {
install(ContentNegotiation) {
json()
json(json = Util.kotlinxConfig)
}
install(Kompendium) {
spec = AuthMetadata.spec
spec = Util.baseSpec
}
install(Authentication) {
// We can leverage the security config name to prevent typos
@ -73,36 +68,6 @@ private fun Application.mainModule() {
}
}
object AuthMetadata {
val spec = OpenApiSpec(
info = Info(
title = "Simple API with documented Authentication",
version = "1.33.7",
description = "Wow isn't this cool?",
termsOfService = URI("https://example.com"),
contact = Contact(
name = "Homer Simpson",
email = "chunkylover53@aol.com",
url = URI("https://gph.is/1NPUDiM")
),
license = License(
name = "MIT",
url = URI("https://github.com/bkbnio/kompendium/blob/main/LICENSE")
)
),
servers = mutableListOf(
Server(
url = URI("https://myawesomeapi.com"),
description = "Production instance of my API"
),
Server(
url = URI("https://staging.myawesomeapi.com"),
description = "Where the fun stuff happens"
)
)
)
}
// This is where we define the available security configurations for our app
object SecurityConfigurations {
val basic = object : BasicAuthConfiguration {

View File

@ -1,5 +1,7 @@
package io.bkbn.kompendium.playground
import com.fasterxml.jackson.annotation.JsonProperty
import com.google.gson.annotations.SerializedName
import io.bkbn.kompendium.annotations.Field
import io.bkbn.kompendium.annotations.Param
import io.bkbn.kompendium.annotations.ParamType
@ -13,18 +15,15 @@ import io.bkbn.kompendium.core.metadata.method.DeleteInfo
import io.bkbn.kompendium.core.metadata.method.GetInfo
import io.bkbn.kompendium.core.metadata.method.PostInfo
import io.bkbn.kompendium.core.routes.redoc
import io.bkbn.kompendium.oas.OpenApiSpec
import io.bkbn.kompendium.oas.info.Contact
import io.bkbn.kompendium.oas.info.Info
import io.bkbn.kompendium.oas.info.License
import io.bkbn.kompendium.oas.server.Server
import io.bkbn.kompendium.oas.serialization.KompendiumSerializersModule
import io.bkbn.kompendium.playground.BasicModels.BasicParameters
import io.bkbn.kompendium.playground.BasicModels.BasicResponse
import io.bkbn.kompendium.playground.BasicModels.BasicRequest
import io.bkbn.kompendium.playground.BasicModels.BasicResponse
import io.bkbn.kompendium.playground.BasicPlaygroundToC.simpleDeleteRequest
import io.bkbn.kompendium.playground.BasicPlaygroundToC.simpleGetExample
import io.bkbn.kompendium.playground.BasicPlaygroundToC.simpleGetExampleWithParameters
import io.bkbn.kompendium.playground.BasicPlaygroundToC.simplePostRequest
import io.bkbn.kompendium.playground.util.Util
import io.ktor.application.Application
import io.ktor.application.call
import io.ktor.application.install
@ -37,8 +36,9 @@ import io.ktor.routing.routing
import io.ktor.serialization.json
import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.net.URI
import kotlinx.serialization.json.Json
import java.util.UUID
/**
@ -57,11 +57,11 @@ fun main() {
private fun Application.mainModule() {
// Installs Simple JSON Content Negotiation
install(ContentNegotiation) {
json()
json(json = Util.kotlinxConfig)
}
// Installs the Kompendium Plugin and sets up baseline server metadata
install(Kompendium) {
spec = BasicMetadata.spec
spec = Util.baseSpec
}
// Configures the routes for our API
routing {
@ -161,38 +161,6 @@ object BasicPlaygroundToC {
)
}
// Contains the root metadata for our server. This is all the stuff that is defined once
// and cannot be inferred from the Ktor application
object BasicMetadata {
val spec = OpenApiSpec(
info = Info(
title = "Simple Demo API",
version = "1.33.7",
description = "Wow isn't this cool?",
termsOfService = URI("https://example.com"),
contact = Contact(
name = "Homer Simpson",
email = "chunkylover53@aol.com",
url = URI("https://gph.is/1NPUDiM")
),
license = License(
name = "MIT",
url = URI("https://github.com/bkbnio/kompendium/blob/main/LICENSE")
)
),
servers = mutableListOf(
Server(
url = URI("https://myawesomeapi.com"),
description = "Production instance of my API"
),
Server(
url = URI("https://staging.myawesomeapi.com"),
description = "Where the fun stuff happens"
)
)
)
}
object BasicModels {
@Serializable
data class BasicResponse(val c: String)
@ -207,6 +175,9 @@ object BasicModels {
@Serializable
data class BasicRequest(
@JsonProperty("best_field")
@SerializedName("best_field")
@SerialName("best_field")
@Field(description = "This is a super important field!!", name = "best_field")
val d: Boolean
)

View File

@ -23,16 +23,12 @@ import io.bkbn.kompendium.core.metadata.ResponseInfo
import io.bkbn.kompendium.core.metadata.method.GetInfo
import io.bkbn.kompendium.core.metadata.method.PostInfo
import io.bkbn.kompendium.core.routes.redoc
import io.bkbn.kompendium.oas.OpenApiSpec
import io.bkbn.kompendium.oas.info.Contact
import io.bkbn.kompendium.oas.info.Info
import io.bkbn.kompendium.oas.info.License
import io.bkbn.kompendium.oas.server.Server
import io.bkbn.kompendium.playground.ConstrainedModels.ConstrainedParams
import io.bkbn.kompendium.playground.ConstrainedModels.ConstrainedRequest
import io.bkbn.kompendium.playground.ConstrainedModels.ConstrainedResponse
import io.bkbn.kompendium.playground.ConstrainedPlaygroundToC.simpleConstrainedGet
import io.bkbn.kompendium.playground.ConstrainedPlaygroundToC.simpleConstrainedPost
import io.bkbn.kompendium.playground.util.Util
import io.ktor.application.Application
import io.ktor.application.call
import io.ktor.application.install
@ -45,7 +41,6 @@ import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonElement
import java.net.URI
fun main() {
embeddedServer(
@ -59,11 +54,11 @@ fun main() {
private fun Application.mainModule() {
// Installs Simple JSON Content Negotiation
install(ContentNegotiation) {
json()
json(json = Util.kotlinxConfig)
}
// Installs the Kompendium Plugin and sets up baseline server metadata
install(Kompendium) {
spec = ConstrainedMetadata.spec
spec = Util.baseSpec
}
// Configures the routes for our API
routing {
@ -104,36 +99,6 @@ object ConstrainedPlaygroundToC {
)
}
object ConstrainedMetadata {
val spec = OpenApiSpec(
info = Info(
title = "Simple Demo API",
version = "1.33.7",
description = "Wow isn't this cool?",
termsOfService = URI("https://example.com"),
contact = Contact(
name = "Homer Simpson",
email = "chunkylover53@aol.com",
url = URI("https://gph.is/1NPUDiM")
),
license = License(
name = "MIT",
url = URI("https://github.com/bkbnio/kompendium/blob/main/LICENSE")
)
),
servers = mutableListOf(
Server(
url = URI("https://myawesomeapi.com"),
description = "Production instance of my API"
),
Server(
url = URI("https://staging.myawesomeapi.com"),
description = "Where the fun stuff happens"
)
)
)
}
object ConstrainedModels {
@Serializable
data class ConstrainedResponse(

View File

@ -6,12 +6,8 @@ import io.bkbn.kompendium.core.metadata.ExceptionInfo
import io.bkbn.kompendium.core.metadata.ResponseInfo
import io.bkbn.kompendium.core.metadata.method.GetInfo
import io.bkbn.kompendium.core.routes.redoc
import io.bkbn.kompendium.oas.OpenApiSpec
import io.bkbn.kompendium.oas.info.Contact
import io.bkbn.kompendium.oas.info.Info
import io.bkbn.kompendium.oas.info.License
import io.bkbn.kompendium.oas.server.Server
import io.bkbn.kompendium.playground.ExceptionPlaygroundToC.simpleGetExample
import io.bkbn.kompendium.playground.util.Util
import io.ktor.application.Application
import io.ktor.application.call
import io.ktor.application.install
@ -25,7 +21,6 @@ import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty
import kotlin.reflect.typeOf
import kotlinx.serialization.Serializable
import java.net.URI
import java.time.LocalDateTime
// Application Entrypoint
@ -41,11 +36,11 @@ fun main() {
private fun Application.mainModule() {
// Installs Simple JSON Content Negotiation
install(ContentNegotiation) {
json()
json(json = Util.kotlinxConfig)
}
// Installs the Kompendium Plugin and sets up baseline server metadata
install(Kompendium) {
spec = ExceptionMetadata.spec
spec = Util.baseSpec
}
install(StatusPages) {
exception<ExceptionModels.BadUserException> {
@ -70,36 +65,6 @@ private fun Application.mainModule() {
}
}
object ExceptionMetadata {
val spec = OpenApiSpec(
info = Info(
title = "Simple Demo API with notarized exceptions",
version = "1.33.7",
description = "Wow isn't this cool?",
termsOfService = URI("https://example.com"),
contact = Contact(
name = "Homer Simpson",
email = "chunkylover53@aol.com",
url = URI("https://gph.is/1NPUDiM")
),
license = License(
name = "MIT",
url = URI("https://github.com/bkbnio/kompendium/blob/main/LICENSE")
)
),
servers = mutableListOf(
Server(
url = URI("https://myawesomeapi.com"),
description = "Production instance of my API"
),
Server(
url = URI("https://staging.myawesomeapi.com"),
description = "Where the fun stuff happens"
)
)
)
}
// This is a table of contents to hold all the metadata for our various API endpoints
object ExceptionPlaygroundToC {
private val simpleException = ExceptionInfo<ExceptionModels.ExceptionResponse>(

View File

@ -5,12 +5,8 @@ import io.bkbn.kompendium.core.Notarized.notarizedGet
import io.bkbn.kompendium.core.metadata.ResponseInfo
import io.bkbn.kompendium.core.metadata.method.GetInfo
import io.bkbn.kompendium.core.routes.redoc
import io.bkbn.kompendium.oas.OpenApiSpec
import io.bkbn.kompendium.oas.info.Contact
import io.bkbn.kompendium.oas.info.Info
import io.bkbn.kompendium.oas.info.License
import io.bkbn.kompendium.oas.server.Server
import io.bkbn.kompendium.playground.GenericPlaygroundToC.simpleGenericGet
import io.bkbn.kompendium.playground.util.Util
import io.ktor.application.Application
import io.ktor.application.call
import io.ktor.application.install
@ -22,7 +18,6 @@ import io.ktor.serialization.json
import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty
import kotlinx.serialization.Serializable
import java.net.URI
/**
* Application entrypoint. Run this and head on over to `localhost:8081/docs`
@ -40,11 +35,11 @@ fun main() {
private fun Application.mainModule() {
// Installs Simple JSON Content Negotiation
install(ContentNegotiation) {
json()
json(json = Util.kotlinxConfig)
}
// Installs the Kompendium Plugin and sets up baseline server metadata
install(Kompendium) {
spec = GenericMetadata.spec
spec = Util.baseSpec
}
routing {
redoc(pageTitle = "Simple API Docs")
@ -70,38 +65,6 @@ object GenericPlaygroundToC {
)
}
// Contains the root metadata for our server. This is all the stuff that is defined once
// and cannot be inferred from the Ktor application
object GenericMetadata {
val spec = OpenApiSpec(
info = Info(
title = "Simple Demo API with Generic Data",
version = "1.33.7",
description = "Wow isn't this cool?",
termsOfService = URI("https://example.com"),
contact = Contact(
name = "Homer Simpson",
email = "chunkylover53@aol.com",
url = URI("https://gph.is/1NPUDiM")
),
license = License(
name = "MIT",
url = URI("https://github.com/bkbnio/kompendium/blob/main/LICENSE")
)
),
servers = mutableListOf(
Server(
url = URI("https://myawesomeapi.com"),
description = "Production instance of my API"
),
Server(
url = URI("https://staging.myawesomeapi.com"),
description = "Where the fun stuff happens"
)
)
)
}
object GenericModels {
@Serializable
data class Foosy<T, K>(val test: T, val otherThing: List<K>)

View File

@ -0,0 +1,49 @@
package io.bkbn.kompendium.playground
import io.bkbn.kompendium.core.Kompendium
import io.bkbn.kompendium.core.Notarized.notarizedPost
import io.bkbn.kompendium.core.routes.redoc
import io.bkbn.kompendium.playground.util.Util
import io.ktor.application.Application
import io.ktor.application.call
import io.ktor.application.install
import io.ktor.features.ContentNegotiation
import io.ktor.gson.gson
import io.ktor.http.HttpStatusCode
import io.ktor.request.receive
import io.ktor.response.respond
import io.ktor.routing.route
import io.ktor.routing.routing
import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty
fun main() {
embeddedServer(
Netty,
port = 8081,
module = Application::mainModule
).start(wait = true)
}
private fun Application.mainModule() {
install(ContentNegotiation) {
gson {
setPrettyPrinting()
}
}
install(Kompendium) {
spec = Util.baseSpec
}
routing {
redoc()
route("/create") {
notarizedPost(BasicPlaygroundToC.simplePostRequest) {
val request = call.receive<BasicModels.BasicRequest>()
when (request.d) {
true -> call.respond(HttpStatusCode.OK, BasicModels.BasicResponse(c = "So it is true!"))
false -> call.respond(HttpStatusCode.OK, BasicModels.BasicResponse(c = "Oh, I knew it!"))
}
}
}
}
}

View File

@ -0,0 +1,52 @@
package io.bkbn.kompendium.playground
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.databind.SerializationFeature
import io.bkbn.kompendium.core.Kompendium
import io.bkbn.kompendium.core.Notarized.notarizedPost
import io.bkbn.kompendium.core.routes.redoc
import io.bkbn.kompendium.playground.util.Util
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.jackson.jackson
import io.ktor.request.receive
import io.ktor.response.respond
import io.ktor.routing.route
import io.ktor.routing.routing
import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty
fun main() {
embeddedServer(
Netty,
port = 8081,
module = Application::mainModule
).start(wait = true)
}
private fun Application.mainModule() {
install(ContentNegotiation) {
jackson {
enable(SerializationFeature.INDENT_OUTPUT)
setSerializationInclusion(JsonInclude.Include.NON_NULL)
}
}
install(Kompendium) {
spec = Util.baseSpec
}
routing {
redoc()
route("/create") {
notarizedPost(BasicPlaygroundToC.simplePostRequest) {
val request = call.receive<BasicModels.BasicRequest>()
when (request.d) {
true -> call.respond(HttpStatusCode.OK, BasicModels.BasicResponse(c = "So it is true!"))
false -> call.respond(HttpStatusCode.OK, BasicModels.BasicResponse(c = "Oh, I knew it!"))
}
}
}
}
}

View File

@ -7,14 +7,10 @@ import io.bkbn.kompendium.core.metadata.ResponseInfo
import io.bkbn.kompendium.core.metadata.method.GetInfo
import io.bkbn.kompendium.core.routes.redoc
import io.bkbn.kompendium.locations.NotarizedLocation.notarizedGet
import io.bkbn.kompendium.oas.OpenApiSpec
import io.bkbn.kompendium.oas.info.Contact
import io.bkbn.kompendium.oas.info.Info
import io.bkbn.kompendium.oas.info.License
import io.bkbn.kompendium.oas.server.Server
import io.bkbn.kompendium.playground.LocationsToC.ohBoiUCrazy
import io.bkbn.kompendium.playground.LocationsToC.testLocation
import io.bkbn.kompendium.playground.LocationsToC.testNestLocation
import io.bkbn.kompendium.playground.util.Util
import io.ktor.application.Application
import io.ktor.application.call
import io.ktor.application.install
@ -28,7 +24,6 @@ import io.ktor.serialization.json
import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty
import kotlinx.serialization.Serializable
import java.net.URI
/**
* Application entrypoint. Run this and head on over to `localhost:8081/docs`
@ -44,10 +39,10 @@ fun main() {
private fun Application.mainModule() {
install(ContentNegotiation) {
json()
json(json = Util.kotlinxConfig)
}
install(Kompendium) {
spec = LocationMetadata.spec
spec = Util.baseSpec
}
install(Locations)
routing {
@ -118,38 +113,6 @@ data class TestLocations(
}
}
// Contains the root metadata for our server. This is all the stuff that is defined once
// and cannot be inferred from the Ktor application
object LocationMetadata {
val spec = OpenApiSpec(
info = Info(
title = "Simple Demo Leveraging Ktor Locations",
version = "1.33.7",
description = "Wow isn't this cool?",
termsOfService = URI("https://example.com"),
contact = Contact(
name = "Homer Simpson",
email = "chunkylover53@aol.com",
url = URI("https://gph.is/1NPUDiM")
),
license = License(
name = "MIT",
url = URI("https://github.com/bkbnio/kompendium/blob/main/LICENSE")
)
),
servers = mutableListOf(
Server(
url = URI("https://myawesomeapi.com"),
description = "Production instance of my API"
),
Server(
url = URI("https://staging.myawesomeapi.com"),
description = "Where the fun stuff happens"
)
)
)
}
object LocationModels {
@Serializable
data class ExampleResponse(val c: String)

View File

@ -5,12 +5,8 @@ import io.bkbn.kompendium.core.Notarized.notarizedGet
import io.bkbn.kompendium.core.metadata.ResponseInfo
import io.bkbn.kompendium.core.metadata.method.GetInfo
import io.bkbn.kompendium.core.routes.redoc
import io.bkbn.kompendium.oas.OpenApiSpec
import io.bkbn.kompendium.oas.info.Contact
import io.bkbn.kompendium.oas.info.Info
import io.bkbn.kompendium.oas.info.License
import io.bkbn.kompendium.oas.server.Server
import io.bkbn.kompendium.playground.PolymorphicPlaygroundToC.polymorphicExample
import io.bkbn.kompendium.playground.util.Util
import io.ktor.application.Application
import io.ktor.application.call
import io.ktor.application.install
@ -22,7 +18,6 @@ import io.ktor.serialization.json
import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty
import kotlinx.serialization.Serializable
import java.net.URI
/**
* Application entrypoint. Run this and head on over to `localhost:8081/docs`
@ -39,11 +34,11 @@ fun main() {
private fun Application.mainModule() {
// Installs Simple JSON Content Negotiation
install(ContentNegotiation) {
json()
json(json = Util.kotlinxConfig)
}
// Installs the Kompendium Plugin and sets up baseline server metadata
install(Kompendium) {
spec = PolymorphicMetadata.spec
spec = Util.baseSpec
}
// Configures the routes for our API
routing {
@ -70,36 +65,6 @@ object PolymorphicPlaygroundToC {
)
}
object PolymorphicMetadata {
val spec = OpenApiSpec(
info = Info(
title = "Simple Demo API with Polymorphic Models",
version = "1.33.7",
description = "Wow isn't this cool?",
termsOfService = URI("https://example.com"),
contact = Contact(
name = "Homer Simpson",
email = "chunkylover53@aol.com",
url = URI("https://gph.is/1NPUDiM")
),
license = License(
name = "MIT",
url = URI("https://github.com/bkbnio/kompendium/blob/main/LICENSE")
)
),
servers = mutableListOf(
Server(
url = URI("https://myawesomeapi.com"),
description = "Production instance of my API"
),
Server(
url = URI("https://staging.myawesomeapi.com"),
description = "Where the fun stuff happens"
)
)
)
}
object PolymorphicModels {
sealed interface SlammaJamma

View File

@ -2,6 +2,7 @@ package io.bkbn.kompendium.playground
import io.bkbn.kompendium.core.Kompendium
import io.bkbn.kompendium.core.Notarized.notarizedGet
import io.bkbn.kompendium.playground.util.Util
import io.bkbn.kompendium.swagger.swaggerUI
import io.ktor.application.Application
import io.ktor.application.call
@ -32,12 +33,12 @@ fun main() {
private fun Application.mainModule() {
// Installs Simple JSON Content Negotiation
install(ContentNegotiation) {
json()
json(json = Util.kotlinxConfig)
}
install(Webjars)
// Installs the Kompendium Plugin and sets up baseline server metadata
install(Kompendium) {
spec = BasicMetadata.spec
spec = Util.baseSpec
}
// Configures the routes for our API
routing {

View File

@ -0,0 +1,50 @@
package io.bkbn.kompendium.playground.util
import io.bkbn.kompendium.oas.OpenApiSpec
import io.bkbn.kompendium.oas.info.Contact
import io.bkbn.kompendium.oas.info.Info
import io.bkbn.kompendium.oas.info.License
import io.bkbn.kompendium.oas.serialization.KompendiumSerializersModule
import io.bkbn.kompendium.oas.server.Server
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import java.net.URI
@OptIn(ExperimentalSerializationApi::class)
object Util {
val kotlinxConfig = Json {
classDiscriminator = "class"
serializersModule = KompendiumSerializersModule.module
prettyPrint = true
explicitNulls = false
encodeDefaults = true
}
val baseSpec = OpenApiSpec(
info = Info(
title = "Simple Demo API",
version = "1.33.7",
description = "Wow isn't this cool?",
termsOfService = URI("https://example.com"),
contact = Contact(
name = "Homer Simpson",
email = "chunkylover53@aol.com",
url = URI("https://gph.is/1NPUDiM")
),
license = License(
name = "MIT",
url = URI("https://github.com/bkbnio/kompendium/blob/main/LICENSE")
)
),
servers = mutableListOf(
Server(
url = URI("https://myawesomeapi.com"),
description = "Production instance of my API"
),
Server(
url = URI("https://staging.myawesomeapi.com"),
description = "Where the fun stuff happens"
)
)
)
}

View File

@ -1 +1,3 @@
# Module kompendium-swagger-ui
Contains the code necessary to launch `swagger` as your documentation frontend.

View File

@ -1,5 +1,24 @@
plugins {
id("io.bkbn.sourdough.library")
kotlin("jvm")
id("io.bkbn.sourdough.library.jvm") version "0.5.4"
id("io.gitlab.arturbosch.detekt") version "1.19.0"
id("com.adarshr.test-logger") version "3.1.0"
id("org.jetbrains.dokka")
id("maven-publish")
id("java-library")
id("signing")
}
sourdough {
githubOrg.set("bkbnio")
githubRepo.set("kompendium")
libraryName.set("Kompendium Swagger")
libraryDescription.set("Offers Swagger as a bundled WebJAR for Ktor")
licenseName.set("MIT License")
licenseUrl.set("https://mit-license.org")
developerId.set("unredundant")
developerName.set("Ryan Brink")
developerEmail.set("admin@bkbn.io")
}
dependencies {
@ -8,3 +27,11 @@ dependencies {
implementation(group = "io.ktor", name = "ktor-webjars", version = ktorVersion)
implementation(group = "org.webjars", name = "swagger-ui", version = "4.1.3")
}
testing {
suites {
named("test", JvmTestSuite::class) {
useJUnitJupiter()
}
}
}

15
renovate.json Normal file
View File

@ -0,0 +1,15 @@
{
"extends": [
"config:base"
],
"packageRules": [
{
"matchUpdateTypes": ["minor", "patch", "pin", "digest"],
"automerge": true
},
{
"matchPackagePatterns": ["ktor"],
"groupName": "ktor"
}
]
}