Compare commits
28 Commits
v2.0.0-alp
...
v2.0.0
Author | SHA1 | Date | |
---|---|---|---|
3c585c06a3 | |||
7bfd168d74 | |||
19298f4deb | |||
54bdf107e2 | |||
721302d651 | |||
3ffda43a52 | |||
ae2a1b578a | |||
147c7e7fb0 | |||
906b329c2e | |||
1bf81cfd82 | |||
c9f173d6b0 | |||
c43fafae1b | |||
3b2fa72d26 | |||
91d4ec10b7 | |||
aa1b898b22 | |||
aa21c1219b | |||
bc380077fb | |||
fc9929e9cc | |||
a26ad72b67 | |||
38a70e4979 | |||
4a1425b73b | |||
01c118373e | |||
7535d67661 | |||
eb369dcdc8 | |||
da104d0a63 | |||
c6ed261fe4 | |||
012db5ad26 | |||
f02f7ad211 |
6
.github/workflows/pr_checks.yml
vendored
6
.github/workflows/pr_checks.yml
vendored
@ -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
|
||||
|
5
.github/workflows/publish.yml
vendored
5
.github/workflows/publish.yml
vendored
@ -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 }}
|
||||
|
53
.github/workflows/release.yml
vendored
53
.github/workflows/release.yml
vendored
@ -1,4 +1,4 @@
|
||||
name: Publish to GitHub Packages
|
||||
name: Publish Release
|
||||
on:
|
||||
release:
|
||||
types:
|
||||
@ -8,25 +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
|
||||
org.gradle.vfs.watch=false
|
||||
org.gradle.vfs.verbose=false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish-to-nexus:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@ -35,12 +16,38 @@ jobs:
|
||||
with:
|
||||
distribution: 'adopt'
|
||||
java-version: '17'
|
||||
- name: Publlish to GithubPackages
|
||||
- name: Publlish to Maven Central
|
||||
uses: burrunan/gradle-cache-action@v1
|
||||
with:
|
||||
gradle-version: wrapper
|
||||
arguments: publishToSonatype closeAndReleaseSonatypeStagingRepository
|
||||
properties: |
|
||||
release=true
|
||||
org.gradle.vfs.watch=false
|
||||
org.gradle.vfs.verbose=false
|
||||
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@v8
|
||||
with:
|
||||
default_author: github_actions
|
||||
branch: main
|
||||
message: 'doc: Added Latest Documentation ✨'
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,5 +1,3 @@
|
||||
.gradle
|
||||
build
|
||||
.idea
|
||||
dokka
|
||||
wiki
|
||||
|
36
CHANGELOG.md
36
CHANGELOG.md
@ -1,6 +1,7 @@
|
||||
# Changelog
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Added
|
||||
|
||||
### Changed
|
||||
@ -11,8 +12,19 @@
|
||||
|
||||
## Released
|
||||
|
||||
## [2.0.0-alpha] - January 2nd, 2021
|
||||
## [2.0.0] - January 23rd, 2022
|
||||
|
||||
Major Release 🎉 As we head towards the Ktor 2 release, this library will be kept compatible with Ktor 1. A future
|
||||
Kompendium 2 repository will be created soon, porting much of the changes you see here, with some awesome Ktor 2 twists
|
||||
😉
|
||||
|
||||
### 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
|
||||
- Support for OAuth authentication
|
||||
- Gradle Toolchain feature to ensure match between local JDK and compile target
|
||||
- Dokka integration
|
||||
@ -22,6 +34,10 @@
|
||||
- Ability to document expected unstructured data
|
||||
|
||||
### 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
|
||||
- `$ref` types are no longer generated, instead all objects are defined explicitly
|
||||
- All OpenAPI domain models moved to a separate module `kompendium-oas`
|
||||
- Moved all files in `kompendium-core` into `io.bkbn.kompendium.core` package from `io.bkbn.kompendium`
|
||||
@ -29,7 +45,8 @@
|
||||
- Gradle build logic offloaded to Sourdough Plugin
|
||||
- Minimum supported Java version is now 11
|
||||
- Bumped Kotlin to 1.6
|
||||
- Annotations now live in a separate module. (Should not impact end users as module is imported as api dependency by core).
|
||||
- Annotations now live in a separate module. (Should not impact end users as module is imported as api dependency by
|
||||
core).
|
||||
- Kotest as the testing framework of choice
|
||||
- Path calculation removed in favor of built-in route toString
|
||||
- Ktor to 1.6.7
|
||||
@ -55,11 +72,15 @@
|
||||
- Dropped ASDF tool manifest
|
||||
|
||||
## [1.11.1] - November 25th, 2021
|
||||
|
||||
### Added
|
||||
|
||||
- Documentation showing how to add header names using Kotlin backtick convention
|
||||
|
||||
## [1.11.0] - November 25th, 2021
|
||||
|
||||
### Added
|
||||
|
||||
- Support for Ktor Location Plugin
|
||||
|
||||
## [1.10.0] - November 25th, 2021
|
||||
@ -89,7 +110,6 @@
|
||||
|
||||
- ByteArray added to the set of default types
|
||||
|
||||
|
||||
## [1.8.1] - October 4th, 2021
|
||||
|
||||
### Added
|
||||
@ -209,7 +229,7 @@ This is just to get my repo back to normal now that I have confirmed sonatype pu
|
||||
|
||||
### Added
|
||||
|
||||
- Support for example request and response bodies. Parameter examples / defaults are a separate issue for later.
|
||||
- Support for example request and response bodies. Parameter examples / defaults are a separate issue for later.
|
||||
|
||||
### Changed
|
||||
|
||||
@ -258,7 +278,8 @@ This is just to get my repo back to normal now that I have confirmed sonatype pu
|
||||
|
||||
### Added
|
||||
|
||||
- Added an explicit `PathCalculator` interface to allow for easier handling of routes external to the core set of Ktor route selectors.
|
||||
- Added an explicit `PathCalculator` interface to allow for easier handling of routes external to the core set of Ktor
|
||||
route selectors.
|
||||
|
||||
## [0.5.1] - April 19th, 2021
|
||||
|
||||
@ -349,12 +370,13 @@ This is just to get my repo back to normal now that I have confirmed sonatype pu
|
||||
|
||||
### Added
|
||||
|
||||
- Beginning of an implementation. Currently, able to generate a rough outline of the API at runtime, along with generating
|
||||
full data classes represented by JSON Schema.
|
||||
- Beginning of an implementation. Currently, able to generate a rough outline of the API at runtime, along with
|
||||
generating full data classes represented by JSON Schema.
|
||||
|
||||
## [0.0.1] - April 11th, 2021
|
||||
|
||||
### Added
|
||||
|
||||
- Added _most_ of the data classes necessary for generating an [Open API Spec](https://swagger.io/specification)
|
||||
- Added playground to allow users to tinker with a live Ktor api in conjunction with development
|
||||
- Added all standard OSS files
|
||||
|
41
Project.md
Normal file
41
Project.md
Normal 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.
|
@ -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
|
||||
|
||||
|
@ -1,14 +1,13 @@
|
||||
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
|
||||
kotlin("plugin.serialization") version "1.6.10" apply false
|
||||
id("io.bkbn.sourdough.library.jvm") version "0.6.0" apply false
|
||||
id("io.bkbn.sourdough.application.jvm") version "0.6.0" apply false
|
||||
id("io.bkbn.sourdough.root") version "0.6.0"
|
||||
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 {
|
||||
@ -33,17 +32,15 @@ allprojects {
|
||||
}
|
||||
|
||||
subprojects {
|
||||
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")
|
||||
plugins.withType(io.bkbn.sourdough.gradle.library.jvm.LibraryJvmPlugin::class) {
|
||||
extensions.configure(io.bkbn.sourdough.gradle.library.jvm.LibraryJvmExtension::class) {
|
||||
githubOrg.set("bkbnio")
|
||||
githubRepo.set("kompendium")
|
||||
licenseName.set("MIT License")
|
||||
licenseUrl.set("https://mit-license.org")
|
||||
developerId.set("unredundant")
|
||||
developerName.set("Ryan Brink")
|
||||
developerEmail.set("admin@bkbn.io")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
# Kompendium
|
||||
project.version=2.0.0-alpha
|
||||
project.version=2.0.0
|
||||
# Kotlin
|
||||
kotlin.code.style=official
|
||||
# Gradle
|
||||
@ -9,4 +9,4 @@ org.gradle.jvmargs=-Xmx2000m
|
||||
|
||||
# Dependencies
|
||||
ktorVersion=1.6.7
|
||||
kotestVersion=5.0.3
|
||||
kotestVersion=5.1.0
|
||||
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.2-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
@ -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.
|
||||
|
@ -1,3 +1,23 @@
|
||||
plugins {
|
||||
id("io.bkbn.sourdough.library")
|
||||
kotlin("jvm")
|
||||
id("io.bkbn.sourdough.library.jvm")
|
||||
id("io.gitlab.arturbosch.detekt")
|
||||
id("com.adarshr.test-logger")
|
||||
id("org.jetbrains.dokka")
|
||||
id("maven-publish")
|
||||
id("java-library")
|
||||
id("signing")
|
||||
}
|
||||
|
||||
sourdough {
|
||||
libraryName.set("Kompendium Annotations")
|
||||
libraryDescription.set("A set of annotations used by Kompendium to generate OpenAPI Specifications")
|
||||
}
|
||||
|
||||
testing {
|
||||
suites {
|
||||
named("test", JvmTestSuite::class) {
|
||||
useJUnitJupiter()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -1,7 +1,18 @@
|
||||
plugins {
|
||||
id("io.bkbn.sourdough.library")
|
||||
kotlin("jvm")
|
||||
id("io.bkbn.sourdough.library.jvm")
|
||||
id("io.gitlab.arturbosch.detekt")
|
||||
id("com.adarshr.test-logger")
|
||||
id("org.jetbrains.dokka")
|
||||
id("maven-publish")
|
||||
id("java-library")
|
||||
id("signing")
|
||||
}
|
||||
|
||||
sourdough {
|
||||
libraryName.set("Kompendium Authentication")
|
||||
libraryDescription.set("Kompendium library to pair with Ktor Auth to provide authorization info to OpenAPI")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// IMPLEMENTATION
|
||||
@ -16,3 +27,11 @@ dependencies {
|
||||
|
||||
testImplementation(testFixtures(projects.kompendiumCore))
|
||||
}
|
||||
|
||||
testing {
|
||||
suites {
|
||||
named("test", JvmTestSuite::class) {
|
||||
useJUnitJupiter()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -1,21 +1,34 @@
|
||||
plugins {
|
||||
id("io.bkbn.sourdough.library")
|
||||
`java-test-fixtures`
|
||||
kotlin("jvm")
|
||||
id("io.bkbn.sourdough.library.jvm")
|
||||
id("io.gitlab.arturbosch.detekt")
|
||||
id("com.adarshr.test-logger")
|
||||
id("org.jetbrains.dokka")
|
||||
id("maven-publish")
|
||||
id("java-library")
|
||||
id("signing")
|
||||
id("java-test-fixtures")
|
||||
}
|
||||
|
||||
sourdough {
|
||||
libraryName.set("Kompendium Core")
|
||||
libraryDescription.set("Core functionality for the Kompendium library")
|
||||
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 +42,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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,3 @@
|
||||
package io.bkbn.kompendium.core.metadata
|
||||
|
||||
data class ParameterExample(val parameterName: String, val exampleName: String, val exampleValue: Any)
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
@ -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
|
||||
}
|
||||
|
@ -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>
|
@ -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>
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -0,0 +1,3 @@
|
||||
package io.bkbn.kompendium.core.parser
|
||||
|
||||
object DefaultMethodParser : IMethodParser
|
@ -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("/\\(.+\\)"), "")
|
||||
}
|
@ -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") {
|
||||
|
@ -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 🌊" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -80,7 +80,7 @@
|
||||
},
|
||||
"required": [
|
||||
"org",
|
||||
"amazingField",
|
||||
"amazing_field",
|
||||
"tables"
|
||||
],
|
||||
"type": "object"
|
||||
|
97
kompendium-core/src/test/resources/example_parameters.json
Normal file
97
kompendium-core/src/test/resources/example_parameters.json
Normal 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": []
|
||||
}
|
@ -62,7 +62,7 @@
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"fieldName",
|
||||
"field_name",
|
||||
"b",
|
||||
"aaa"
|
||||
],
|
||||
|
@ -45,7 +45,7 @@
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"b"
|
||||
"real_name"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
|
49
kompendium-core/src/test/resources/notarized_head.json
Normal file
49
kompendium-core/src/test/resources/notarized_head.json
Normal 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": []
|
||||
}
|
84
kompendium-core/src/test/resources/notarized_options.json
Normal file
84
kompendium-core/src/test/resources/notarized_options.json
Normal 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": []
|
||||
}
|
64
kompendium-core/src/test/resources/notarized_patch.json
Normal file
64
kompendium-core/src/test/resources/notarized_patch.json
Normal 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": []
|
||||
}
|
@ -82,7 +82,7 @@
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"fieldName",
|
||||
"field_name",
|
||||
"b",
|
||||
"aaa"
|
||||
],
|
||||
|
@ -82,7 +82,7 @@
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"fieldName",
|
||||
"field_name",
|
||||
"b",
|
||||
"aaa"
|
||||
],
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -1,5 +1,18 @@
|
||||
plugins {
|
||||
id("io.bkbn.sourdough.library")
|
||||
kotlin("jvm")
|
||||
id("io.bkbn.sourdough.library.jvm")
|
||||
id("io.gitlab.arturbosch.detekt")
|
||||
id("com.adarshr.test-logger")
|
||||
id("org.jetbrains.dokka")
|
||||
id("maven-publish")
|
||||
id("java-library")
|
||||
id("signing")
|
||||
}
|
||||
|
||||
sourdough {
|
||||
libraryName.set("Kompendium Locations")
|
||||
libraryDescription.set("Supplemental library for Kompendium offering support for Ktor's Location API")
|
||||
compilerArgs.set(listOf("-opt-in=kotlin.RequiresOptIn"))
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@ -14,3 +27,11 @@ dependencies {
|
||||
|
||||
testImplementation(testFixtures(projects.kompendiumCore))
|
||||
}
|
||||
|
||||
testing {
|
||||
suites {
|
||||
named("test", JvmTestSuite::class) {
|
||||
useJUnitJupiter()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
)
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -40,6 +40,15 @@
|
||||
},
|
||||
"required": true,
|
||||
"deprecated": false
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"required": true,
|
||||
"deprecated": false
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
|
@ -40,6 +40,15 @@
|
||||
},
|
||||
"required": true,
|
||||
"deprecated": false
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"required": true,
|
||||
"deprecated": false
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
|
@ -40,6 +40,15 @@
|
||||
},
|
||||
"required": true,
|
||||
"deprecated": false
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"required": true,
|
||||
"deprecated": false
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
|
@ -40,6 +40,15 @@
|
||||
},
|
||||
"required": true,
|
||||
"deprecated": false
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"required": true,
|
||||
"deprecated": false
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
|
@ -1,3 +1,29 @@
|
||||
plugins {
|
||||
id("io.bkbn.sourdough.library")
|
||||
kotlin("jvm")
|
||||
kotlin("plugin.serialization")
|
||||
id("io.bkbn.sourdough.library.jvm")
|
||||
id("io.gitlab.arturbosch.detekt")
|
||||
id("com.adarshr.test-logger")
|
||||
id("org.jetbrains.dokka")
|
||||
id("maven-publish")
|
||||
id("java-library")
|
||||
id("signing")
|
||||
}
|
||||
|
||||
sourdough {
|
||||
libraryName.set("Kompendium OpenAPI Spec")
|
||||
libraryDescription.set("Collections of kotlin data classes modeling the OpenAPI specification")
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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?
|
||||
)
|
||||
|
@ -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,
|
||||
|
@ -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()
|
||||
)
|
||||
|
@ -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?
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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())
|
||||
}
|
||||
|
||||
}
|
@ -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())
|
||||
}
|
||||
}
|
@ -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())
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -1,6 +1,12 @@
|
||||
plugins {
|
||||
kotlin("plugin.serialization") version "1.6.0"
|
||||
application
|
||||
kotlin("jvm")
|
||||
kotlin("plugin.serialization")
|
||||
id("io.bkbn.sourdough.application.jvm")
|
||||
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.33")
|
||||
implementation("org.slf4j:slf4j-simple:1.7.33")
|
||||
|
||||
|
||||
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()
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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(
|
||||
|
@ -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>(
|
||||
|
@ -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>)
|
||||
|
@ -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!"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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!"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
@ -1 +1,3 @@
|
||||
# Module kompendium-swagger-ui
|
||||
|
||||
Contains the code necessary to launch `swagger` as your documentation frontend.
|
||||
|
@ -1,10 +1,30 @@
|
||||
plugins {
|
||||
id("io.bkbn.sourdough.library")
|
||||
kotlin("jvm")
|
||||
id("io.bkbn.sourdough.library.jvm")
|
||||
id("io.gitlab.arturbosch.detekt")
|
||||
id("com.adarshr.test-logger")
|
||||
id("org.jetbrains.dokka")
|
||||
id("maven-publish")
|
||||
id("java-library")
|
||||
id("signing")
|
||||
}
|
||||
|
||||
sourdough {
|
||||
libraryName.set("Kompendium Swagger")
|
||||
libraryDescription.set("Offers Swagger as a bundled WebJAR for Ktor")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
val ktorVersion: String by project
|
||||
implementation(group = "io.ktor", name = "ktor-server-core", version = ktorVersion)
|
||||
implementation(group = "io.ktor", name = "ktor-webjars", version = ktorVersion)
|
||||
implementation(group = "org.webjars", name = "swagger-ui", version = "4.1.3")
|
||||
implementation(group = "org.webjars", name = "swagger-ui", version = "4.2.1")
|
||||
}
|
||||
|
||||
testing {
|
||||
suites {
|
||||
named("test", JvmTestSuite::class) {
|
||||
useJUnitJupiter()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
15
renovate.json
Normal file
15
renovate.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"extends": [
|
||||
"config:base"
|
||||
],
|
||||
"packageRules": [
|
||||
{
|
||||
"matchUpdateTypes": ["minor", "patch", "pin", "digest"],
|
||||
"automerge": true
|
||||
},
|
||||
{
|
||||
"matchPackagePatterns": ["ktor"],
|
||||
"groupName": "ktor"
|
||||
}
|
||||
]
|
||||
}
|
Reference in New Issue
Block a user