Compare commits
38 Commits
v1.2.2
...
v2.0.0-bet
Author | SHA1 | Date | |
---|---|---|---|
aa1b898b22 | |||
aa21c1219b | |||
bc380077fb | |||
fc9929e9cc | |||
a26ad72b67 | |||
38a70e4979 | |||
4a1425b73b | |||
01c118373e | |||
7535d67661 | |||
eb369dcdc8 | |||
da104d0a63 | |||
c6ed261fe4 | |||
012db5ad26 | |||
f02f7ad211 | |||
c29567114d | |||
d66880f9b2 | |||
5e070e1875 | |||
dd780ad29d | |||
d2165d23bf | |||
d9d0f129b5 | |||
c8d56e62a2 | |||
67bd6ad36f | |||
1a924058a1 | |||
8f81b4d795 | |||
9edd3a53ce | |||
91a6164663 | |||
5a7e052ac4 | |||
6ba3617e32 | |||
c32c91829b | |||
b021935b10 | |||
3d99bf35fd | |||
c5f8ace5d2 | |||
b0149c293e | |||
925172cf86 | |||
aa3290243b | |||
2e7dad444b | |||
6e56bf7425 | |||
dfc1593022 |
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**To Reproduce**
|
||||||
|
Steps to reproduce the behavior:
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
**Screenshots**
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
**Desktop (please complete the following information):**
|
||||||
|
- OS: [e.g. iOS]
|
||||||
|
- Browser [e.g. chrome, safari]
|
||||||
|
- Version [e.g. 22]
|
||||||
|
|
||||||
|
**Smartphone (please complete the following information):**
|
||||||
|
- Device: [e.g. iPhone6]
|
||||||
|
- OS: [e.g. iOS8.1]
|
||||||
|
- Browser [e.g. stock browser, safari]
|
||||||
|
- Version [e.g. 22]
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context about the problem here.
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for this project
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
**Describe alternatives you've considered**
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the feature request here.
|
32
.github/workflows/pr_checks.yml
vendored
32
.github/workflows/pr_checks.yml
vendored
@ -5,35 +5,27 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: Set up JDK 11
|
- name: Set up JDK 17
|
||||||
uses: actions/setup-java@v2
|
uses: actions/setup-java@v2
|
||||||
with:
|
with:
|
||||||
distribution: 'adopt'
|
distribution: 'adopt'
|
||||||
java-version: '11'
|
java-version: '17'
|
||||||
- name: Cache Gradle packages
|
- name: Lint
|
||||||
uses: actions/cache@v2
|
uses: burrunan/gradle-cache-action@v1
|
||||||
with:
|
with:
|
||||||
path: ~/.gradle/caches
|
gradle-version: wrapper
|
||||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle.kts') }}
|
arguments: detekt
|
||||||
restore-keys: ${{ runner.os }}-gradle
|
|
||||||
- name: Lint using detekt
|
|
||||||
run: ./gradlew detekt
|
|
||||||
unit:
|
unit:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: Set up JDK 11
|
- name: Set up JDK 17
|
||||||
uses: actions/setup-java@v2
|
uses: actions/setup-java@v2
|
||||||
with:
|
with:
|
||||||
distribution: 'adopt'
|
distribution: 'adopt'
|
||||||
java-version: '11'
|
java-version: '17'
|
||||||
- name: Cache Gradle packages
|
- name: Unit Tests
|
||||||
uses: actions/cache@v2
|
uses: burrunan/gradle-cache-action@v1
|
||||||
with:
|
with:
|
||||||
path: ~/.gradle/caches
|
gradle-version: wrapper
|
||||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle.kts') }}
|
arguments: test koverCollectReports
|
||||||
restore-keys: ${{ runner.os }}-gradle
|
|
||||||
- name: Assemble with Gradle
|
|
||||||
run: ./gradlew assemble
|
|
||||||
- name: Run Unit Tests
|
|
||||||
run: ./gradlew test
|
|
||||||
|
15
.github/workflows/publish.yml
vendored
15
.github/workflows/publish.yml
vendored
@ -2,6 +2,8 @@ name: Publish to GitHub Packages
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ main ]
|
branches: [ main ]
|
||||||
|
paths-ignore:
|
||||||
|
- docs/**
|
||||||
env:
|
env:
|
||||||
ORG_GRADLE_PROJECT_signingKey: ${{ secrets.SONATYPE_SIGNING_KEY }}
|
ORG_GRADLE_PROJECT_signingKey: ${{ secrets.SONATYPE_SIGNING_KEY }}
|
||||||
ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.SONATYPE_SIGNING_PASSWORD }}
|
ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.SONATYPE_SIGNING_PASSWORD }}
|
||||||
@ -13,14 +15,11 @@ jobs:
|
|||||||
- uses: actions/setup-java@v2
|
- uses: actions/setup-java@v2
|
||||||
with:
|
with:
|
||||||
distribution: 'adopt'
|
distribution: 'adopt'
|
||||||
java-version: '11'
|
java-version: '17'
|
||||||
- name: Cache Gradle packages
|
- name: Publish to GitHub Packages
|
||||||
uses: actions/cache@v2
|
uses: burrunan/gradle-cache-action@v1
|
||||||
with:
|
with:
|
||||||
path: ~/.gradle/caches
|
gradle-version: wrapper
|
||||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle.kts') }}
|
arguments: publishAllPublicationsToGithubPackagesRepository
|
||||||
restore-keys: ${{ runner.os }}-gradle
|
|
||||||
- name: Publish package
|
|
||||||
run: ./gradlew publishAllPublicationsToGithubPackagesRepository
|
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
60
.github/workflows/release.yml
vendored
60
.github/workflows/release.yml
vendored
@ -1,4 +1,4 @@
|
|||||||
name: Publish to GitHub Packages
|
name: Publish Release
|
||||||
on:
|
on:
|
||||||
release:
|
release:
|
||||||
types:
|
types:
|
||||||
@ -8,24 +8,6 @@ env:
|
|||||||
ORG_GRADLE_PROJECT_signingKey: ${{ secrets.SONATYPE_SIGNING_KEY }}
|
ORG_GRADLE_PROJECT_signingKey: ${{ secrets.SONATYPE_SIGNING_KEY }}
|
||||||
ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.SONATYPE_SIGNING_PASSWORD }}
|
ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.SONATYPE_SIGNING_PASSWORD }}
|
||||||
jobs:
|
jobs:
|
||||||
publish-to-github:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- uses: actions/setup-java@v2
|
|
||||||
with:
|
|
||||||
distribution: 'adopt'
|
|
||||||
java-version: '11'
|
|
||||||
- name: Cache Gradle packages
|
|
||||||
uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: ~/.gradle/caches
|
|
||||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle.kts') }}
|
|
||||||
restore-keys: ${{ runner.os }}-gradle
|
|
||||||
- name: Publish packages to Github
|
|
||||||
run: ./gradlew publishAllPublicationsToGithubPackagesRepository -Prelease=true
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
publish-to-nexus:
|
publish-to-nexus:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
@ -33,15 +15,39 @@ jobs:
|
|||||||
- uses: actions/setup-java@v2
|
- uses: actions/setup-java@v2
|
||||||
with:
|
with:
|
||||||
distribution: 'adopt'
|
distribution: 'adopt'
|
||||||
java-version: '11'
|
java-version: '17'
|
||||||
- name: Cache Gradle packages
|
- name: Publlish to Maven Central
|
||||||
uses: actions/cache@v2
|
uses: burrunan/gradle-cache-action@v1
|
||||||
with:
|
with:
|
||||||
path: ~/.gradle/caches
|
gradle-version: wrapper
|
||||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle.kts') }}
|
arguments: publishToSonatype closeAndReleaseSonatypeStagingRepository
|
||||||
restore-keys: ${{ runner.os }}-gradle
|
properties: |
|
||||||
- name: Publish packages to Github
|
release=true
|
||||||
run: ./gradlew publishToSonatype closeAndReleaseSonatypeStagingRepository -Prelease=true
|
|
||||||
env:
|
env:
|
||||||
ORG_GRADLE_PROJECT_sonatypeUsername: ${{ secrets.SONATYPE_USER }}
|
ORG_GRADLE_PROJECT_sonatypeUsername: ${{ secrets.SONATYPE_USER }}
|
||||||
ORG_GRADLE_PROJECT_sonatypePassword: ${{ secrets.SONATYPE_PASSWORD }}
|
ORG_GRADLE_PROJECT_sonatypePassword: ${{ secrets.SONATYPE_PASSWORD }}
|
||||||
|
build-documentation:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- publish-to-nexus
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||||
|
- uses: actions/setup-java@v2
|
||||||
|
with:
|
||||||
|
distribution: 'adopt'
|
||||||
|
java-version: '17'
|
||||||
|
- name: Build New Documentation
|
||||||
|
uses: burrunan/gradle-cache-action@v1
|
||||||
|
with:
|
||||||
|
gradle-version: wrapper
|
||||||
|
arguments: dokkaHtmlMultiModule
|
||||||
|
properties: |
|
||||||
|
release=true
|
||||||
|
- name: Push New Documentation
|
||||||
|
uses: EndBug/add-and-commit@v7
|
||||||
|
with:
|
||||||
|
default_author: github_actions
|
||||||
|
branch: main
|
||||||
|
message: 'doc: Added Latest Documentation ✨'
|
||||||
|
6
.gitignore
vendored
6
.gitignore
vendored
@ -1,7 +1,5 @@
|
|||||||
# Ignore Gradle project-specific cache directory
|
|
||||||
.gradle
|
.gradle
|
||||||
|
|
||||||
# Ignore Gradle build output directory
|
|
||||||
build
|
build
|
||||||
|
|
||||||
.idea
|
.idea
|
||||||
|
dokka
|
||||||
|
wiki
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
kotlin 1.5.0-M2
|
|
||||||
java openjdk-14.0.1
|
|
||||||
gradle 7.0
|
|
159
CHANGELOG.md
159
CHANGELOG.md
@ -1,5 +1,164 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
### Added
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
### Remove
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Released
|
||||||
|
|
||||||
|
## [2.0.0-beta] - January 12th, 2022
|
||||||
|
### Added
|
||||||
|
- Support for HTTP Patch, Head, and Options methods
|
||||||
|
- Support for including parameter examples via `MethodInfo`
|
||||||
|
- Dokka Pipeline Generation
|
||||||
|
- GitHub Pages integration
|
||||||
|
- Sourdough Gradle updates
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Kompendium now leverages the chosen API serializer. Supports Jackson, Gson and Kotlinx Serialization
|
||||||
|
- Fixed bug where overridden field names were not reflected in serialized object and required array
|
||||||
|
- Fixed bug where Ktor Location parents were not being scanned for parameters
|
||||||
|
|
||||||
|
## [2.0.0-alpha] - January 2nd, 2022
|
||||||
|
### Added
|
||||||
|
- Support for OAuth authentication
|
||||||
|
- Gradle Toolchain feature to ensure match between local JDK and compile target
|
||||||
|
- Dokka integration
|
||||||
|
- Post-processing callback hook
|
||||||
|
- `description` key to KompendiumField
|
||||||
|
- Set of base constraints for simple and formatted types
|
||||||
|
- Ability to document expected unstructured data
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- `$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`
|
||||||
|
- Gradle bumped to 7.3.2
|
||||||
|
- 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).
|
||||||
|
- Kotest as the testing framework of choice
|
||||||
|
- Path calculation removed in favor of built-in route toString
|
||||||
|
- Ktor to 1.6.7
|
||||||
|
- Completely reworked authentication and exceptions
|
||||||
|
- MethodInfo now exists in a separate package as a sealed interface, each implementation also has its own file
|
||||||
|
- Kompendium is now a Ktor Plugin!
|
||||||
|
- GitHub Actions now leverage Gradle Wrapper
|
||||||
|
- Dropped Codacy support b/c codacy kinda sucks
|
||||||
|
- Fixed bug where KompendiumField was being completely ignored
|
||||||
|
- Redid playground to serve as a showcase for various functionality
|
||||||
|
- README updates
|
||||||
|
- Refactored `handleComplexType` 🎉
|
||||||
|
- Enabled field descriptions
|
||||||
|
- Dropped Version Catalog
|
||||||
|
- Responses are now a map of _actual_ responses rather than generic payloads
|
||||||
|
- Fixed bug where params with default values were listed as required
|
||||||
|
- Made empty put/post request info opt-in rather than default
|
||||||
|
- Fields are now marked as required when there is no default, and they are non-nullable
|
||||||
|
- `KompendiumField` and 'KompendiumParam' renamed to `Field` and `Param` respectively
|
||||||
|
- Description dropped from `Param`
|
||||||
|
- Dropped unnecessary parameter content scanning method
|
||||||
|
- Fixed bug causing all request bodies to be marked as optional
|
||||||
|
- 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
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Added `operationId` parameter to `MethodInfo`
|
||||||
|
|
||||||
|
## [1.9.2] - October 24th, 2021
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Jackson ObjectMapper passed by parameter to openapi module
|
||||||
|
- Added serializable annotation to ExceptionResponse
|
||||||
|
|
||||||
|
## [1.9.1] - October 17th, 2021
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Code Coverage removed from PR checks due to limitations with GitHub workflows
|
||||||
|
- Minor linting fixes
|
||||||
|
- Detekt now builds off of default config
|
||||||
|
|
||||||
|
## [1.9.0] - october 15th, 2021
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- ByteArray added to the set of default types
|
||||||
|
|
||||||
|
|
||||||
|
## [1.8.1] - October 4th, 2021
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Codacy integration
|
||||||
|
|
||||||
|
## [1.8.0] - October 4th, 2021
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Path calculation revamped to allow for simpler selector injection
|
||||||
|
- Kotlin version bumped to 1.5.31
|
||||||
|
- Ktor version bumped to 1.6.4
|
||||||
|
|
||||||
|
## [1.7.0] - August 14th, 2021
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added ability to inject an emergency `UndeclaredField` in the event of certain polymorphic serializers and such
|
||||||
|
|
||||||
|
## [1.6.0] - August 12th, 2021
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Ability to add custom type schema overrides for edge case types.
|
||||||
|
|
||||||
|
## [1.5.1] - August 12th, 2021
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Fixed bug where polymorphic types were not being rendered correctly when part of collections and maps
|
||||||
|
|
||||||
|
## [1.5.0] - July 25th, 2021
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Added support for BigInteger and BigDecimal in response types
|
||||||
|
|
||||||
|
## [1.4.0] - July 22nd, 2021
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Decreased jvmTarget version from 11 to 1.8
|
||||||
|
|
||||||
|
## [1.3.0] - June 4th, 2021
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Explicitly encode JSON object by default
|
||||||
|
|
||||||
|
## [1.2.3] - June 3rd, 2021
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Updates showing/explaining serializer agnostic approach
|
||||||
|
|
||||||
## [1.2.2] - May 23rd, 2021
|
## [1.2.2] - May 23rd, 2021
|
||||||
|
|
||||||
This is just to get my repo back to normal now that I have confirmed sonatype publish is happening
|
This is just to get my repo back to normal now that I have confirmed sonatype publish is happening
|
||||||
|
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.
|
226
README.md
226
README.md
@ -1,215 +1,53 @@
|
|||||||
# Kompendium
|
# Kompendium
|
||||||
|
|
||||||
|
[](https://search.maven.org/search?q=io.bkbn%20kompendium)
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [What Is Kompendium](#what-is-kompendium)
|
||||||
|
- [How to Install](#how-to-install)
|
||||||
|
- [Library Details](#library-details)
|
||||||
|
- [Local Development](#local-development)
|
||||||
|
- [The Playground](#the-playground)
|
||||||
|
|
||||||
## What is Kompendium
|
## What is Kompendium
|
||||||
|
|
||||||
Kompendium is intended to be a minimally invasive OpenApi Specification generator for [Ktor](https://ktor.io).
|
Kompendium is intended to be a minimally invasive OpenApi Specification generator for Ktor. Minimally invasive meaning
|
||||||
Minimally invasive meaning that users will use only Ktor native functions when implementing their API, and will
|
that users will use only Ktor native functions when implementing their API, and will supplement with Kompendium code in
|
||||||
supplement with Kompendium code in order to generate the appropriate spec.
|
order to generate the appropriate spec.
|
||||||
|
|
||||||
## How to install
|
## How to install
|
||||||
|
|
||||||
Kompendium uses GitHub packages as its repository. Installing with Gradle is pretty painless. In your `build.gradle.kts`
|
Kompendium publishes all releases to Maven Central. As such, using the release versions of `Kompendium` is as simple as
|
||||||
add the following
|
declaring it as an implementation dependency in your `build.gradle.kts`
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
// 1 Setup a helper function to import any Github Repository Package
|
|
||||||
// This step is optional but I have a bunch of stuff stored on github so I find it useful 😄
|
|
||||||
fun RepositoryHandler.github(packageUrl: String) = maven {
|
|
||||||
name = "GithubPackages"
|
|
||||||
url = uri(packageUrl)
|
|
||||||
credentials { // TODO Not sure this is necessary for public repositories?
|
|
||||||
username = java.lang.System.getenv("GITHUB_USER")
|
|
||||||
password = java.lang.System.getenv("GITHUB_TOKEN")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2 Add the repo in question (in this case Kompendium)
|
|
||||||
repositories {
|
repositories {
|
||||||
github("https://maven.pkg.github.com/bkbnio/kompendium")
|
mavenCentral()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3 Add the package like any normal dependency
|
dependencies {
|
||||||
dependencies {
|
implementation("io.bkbn:kompendium-core:latest.release")
|
||||||
implementation("io.bkbn:kompendium-core:1.0.0")
|
|
||||||
}
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
## In depth
|
|
||||||
|
|
||||||
### Notarized Routes
|
|
||||||
|
|
||||||
Kompendium introduces the concept of `notarized` HTTP methods. That is, for all your `GET`, `POST`, `PUT`, and `DELETE`
|
|
||||||
operations, there is a corresponding `notarized` method. These operations are strongly typed, and use reification for
|
|
||||||
a lot of the class based reflection that powers Kompendium. Generally speaking the three types that a `notarized` method
|
|
||||||
will consume are
|
|
||||||
|
|
||||||
- `TParam`: Used to notarize expected request parameters
|
|
||||||
- `TReq`: Used to build the object schema for a request body
|
|
||||||
- `TResp`: Used to build the object schema for a response body
|
|
||||||
|
|
||||||
`GET` and `DELETE` take `TParam` and `TResp` while `PUT` and `POST` take all three.
|
|
||||||
|
|
||||||
|
|
||||||
In addition to standard HTTP Methods, Kompendium also introduced the concept of `notarizedExceptions`. Using the `StatusPage`
|
|
||||||
extension, users can notarize all handled exceptions, along with their respective HTTP codes and response types.
|
|
||||||
Exceptions that have been `notarized` require two types as supplemental information
|
|
||||||
|
|
||||||
- `TErr`: Used to notarize the exception being handled by this use case. Used for matching responses at the route level.
|
|
||||||
- `TResp`: Same as above, this dictates the expected return type of the error response.
|
|
||||||
|
|
||||||
In keeping with minimal invasion, these extension methods all consume the same code block as a standard Ktor route method,
|
|
||||||
meaning that swapping in a default Ktor route and a Kompendium `notarized` route is as simple as a single method change.
|
|
||||||
|
|
||||||
### Supplemental Annotations
|
|
||||||
|
|
||||||
In general, Kompendium tries to limit the number of annotations that developers need to use in order to get an app
|
|
||||||
integrated.
|
|
||||||
|
|
||||||
Currently, the annotations used by Kompendium are as follows
|
|
||||||
|
|
||||||
- `KompendiumField`
|
|
||||||
- `KompendiumParam`
|
|
||||||
|
|
||||||
The intended purpose of `KompendiumField` is to offer field level overrides such as naming conventions (ie snake instead of camel).
|
|
||||||
|
|
||||||
The purpose of `KompendiumParam` is to provide supplemental information needed to properly assign the type of parameter
|
|
||||||
(cookie, header, query, path) as well as other parameter-level metadata.
|
|
||||||
|
|
||||||
### Polymorphism
|
|
||||||
|
|
||||||
Out of the box, Kompendium has support for sealed classes. At runtime, it will build a mapping of all available sub-classes
|
|
||||||
and build a spec that takes `anyOf` the implementations. This is currently a weak point of the entire library, and
|
|
||||||
suggestions on better implementations are welcome 🤠
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
The full source code can be found in the `kompendium-playground` module. Here is a simple get endpoint example
|
|
||||||
|
|
||||||
```kotlin
|
|
||||||
// Minimal API Example
|
|
||||||
fun Application.mainModule() {
|
|
||||||
install(StatusPages) {
|
|
||||||
notarizedException<Exception, ExceptionResponse>(
|
|
||||||
info = ResponseInfo(
|
|
||||||
KompendiumHttpCodes.BAD_REQUEST,
|
|
||||||
"Bad Things Happened"
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
call.respond(HttpStatusCode.BadRequest, ExceptionResponse("Why you do dis?"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
routing {
|
|
||||||
openApi(oas)
|
|
||||||
redoc(oas)
|
|
||||||
swaggerUI()
|
|
||||||
route("/potato/spud") {
|
|
||||||
notarizedGet(simpleGetInfo) {
|
|
||||||
call.respond(HttpStatusCode.OK)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val simpleGetInfo = GetInfo<Unit, ExampleResponse>(
|
|
||||||
summary = "Example Parameters",
|
|
||||||
description = "A test for setting parameter examples",
|
|
||||||
responseInfo = ResponseInfo(
|
|
||||||
status = 200,
|
|
||||||
description = "nice",
|
|
||||||
examples = mapOf("test" to ExampleResponse(c = "spud"))
|
|
||||||
),
|
|
||||||
canThrow = setOf(Exception::class)
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Kompendium Auth and security schemes
|
|
||||||
|
|
||||||
There is a separate library to handle security schemes: `kompendium-auth`.
|
|
||||||
This needs to be added to your project as dependency.
|
|
||||||
|
|
||||||
At the moment, the basic and jwt authentication is only supported.
|
|
||||||
|
|
||||||
A minimal example would be:
|
|
||||||
```kotlin
|
|
||||||
install(Authentication) {
|
|
||||||
notarizedBasic("basic") {
|
|
||||||
realm = "Ktor realm 1"
|
|
||||||
// configure basic authentication provider..
|
|
||||||
}
|
|
||||||
notarizedJwt("jwt") {
|
|
||||||
realm = "Ktor realm 2"
|
|
||||||
// configure jwt authentication provider...
|
|
||||||
}
|
|
||||||
}
|
|
||||||
routing {
|
|
||||||
authenticate("basic") {
|
|
||||||
route("/basic_auth") {
|
|
||||||
notarizedGet(basicAuthGetInfo) {
|
|
||||||
call.respondText { "basic auth" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
authenticate("jwt") {
|
|
||||||
route("/jwt") {
|
|
||||||
notarizedGet(jwtAuthGetInfo) {
|
|
||||||
call.respondText { "jwt" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val basicAuthGetInfo = MethodInfo<Unit, ExampleResponse>(
|
|
||||||
summary = "Another get test",
|
|
||||||
description = "testing more",
|
|
||||||
responseInfo = testGetResponse,
|
|
||||||
securitySchemes = setOf("basic")
|
|
||||||
)
|
|
||||||
val jwtAuthGetInfo = basicAuthGetInfo.copy(securitySchemes = setOf("jwt"))
|
|
||||||
```
|
|
||||||
|
|
||||||
### Enabling Swagger ui
|
|
||||||
To enable Swagger UI, `kompendium-swagger-ui` needs to be added.
|
|
||||||
This will also add the [ktor webjars feature](https://ktor.io/docs/webjars.html) to your classpath as it is required for swagger ui.
|
|
||||||
Minimal Example:
|
|
||||||
```kotlin
|
|
||||||
install(Webjars)
|
|
||||||
routing {
|
|
||||||
openApi(oas)
|
|
||||||
swaggerUI()
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Enabling ReDoc
|
|
||||||
Unlike swagger, redoc is provided (perhaps confusingly, in the `core` module). This means out of the box with `kompendium-core`, you can add
|
|
||||||
[ReDoc](https://github.com/Redocly/redoc) as follows
|
|
||||||
|
|
||||||
```kotlin
|
|
||||||
routing {
|
|
||||||
openApi(oas)
|
|
||||||
redoc(oas)
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Limitations
|
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)
|
||||||
|
|
||||||
### Kompendium as a singleton
|
# Library Details
|
||||||
|
|
||||||
Currently, Kompendium exists as a Kotlin object. This comes with a couple perks, but a couple downsides. Primarily,
|
Forthcoming, more details on V2 will be published soon :)
|
||||||
it offers a seriously clean UX where the implementer doesn't need to worry about what instance to send data to. The main
|
|
||||||
drawback, however, is that you are limited to a single API per classpath.
|
|
||||||
|
|
||||||
If this is a blocker, please open a GitHub issue, and we can start to think out solutions!
|
## Local Development
|
||||||
|
|
||||||
## Future Work
|
Kompendium should run locally right out of the box, no configuration necessary (assuming you have JDK 1.8+ installed).
|
||||||
Work on V1 of Kompendium has come to a close. This, however, does not mean it has achieved complete
|
New features can be built locally and published to your local maven repository with the `./gradlew publishToMavenLocal`
|
||||||
parity with the OpenAPI feature spec, nor does it have all-of-the nice to have features that a truly next-gen API spec
|
command!
|
||||||
should have. There are several outstanding features that have been added to the
|
|
||||||
[V2 Milestone](https://github.com/bkbnio/kompendium/milestone/2). Among others, this includes
|
|
||||||
|
|
||||||
- AsyncAPI Integration
|
## The Playground
|
||||||
- Field Validation
|
|
||||||
- MavenCentral Release
|
|
||||||
|
|
||||||
If you have a feature that you would like to see implemented that is not on this list, or discover a 🐞, please open
|
This repo contains a `playground` module that contains a number of working examples showcasing the capabilities of
|
||||||
an issue [here](https://github.com/bkbnio/kompendium/issues/new)
|
Kompendium.
|
||||||
|
|
||||||
|
Feel free to check it out, or even create your own example!
|
||||||
|
@ -1,13 +1,19 @@
|
|||||||
import com.adarshr.gradle.testlogger.theme.ThemeType
|
|
||||||
import com.adarshr.gradle.testlogger.TestLoggerExtension
|
|
||||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
|
||||||
import io.gitlab.arturbosch.detekt.extensions.DetektExtension
|
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("org.jetbrains.kotlin.jvm") version "1.5.0" apply false
|
kotlin("jvm") version "1.6.10" apply false
|
||||||
id("io.gitlab.arturbosch.detekt") version "1.17.0-RC3" apply false
|
id("io.bkbn.sourdough.root") version "0.5.4"
|
||||||
id("com.adarshr.test-logger") version "3.0.0" apply false
|
id("com.github.jakemarsden.git-hooks") version "0.0.2"
|
||||||
id("io.github.gradle-nexus.publish-plugin") version "1.1.0" apply true
|
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 {
|
||||||
|
setHooks(
|
||||||
|
mapOf(
|
||||||
|
"pre-commit" to "detekt",
|
||||||
|
"pre-push" to "test"
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
@ -20,59 +26,4 @@ allprojects {
|
|||||||
else -> "$baseVersion-SNAPSHOT"
|
else -> "$baseVersion-SNAPSHOT"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
repositories {
|
|
||||||
mavenCentral()
|
|
||||||
maven { url = uri("https://maven.pkg.jetbrains.space/public/p/kotlinx-html/maven") }
|
|
||||||
}
|
|
||||||
|
|
||||||
apply(plugin = "org.jetbrains.kotlin.jvm")
|
|
||||||
apply(plugin = "io.gitlab.arturbosch.detekt")
|
|
||||||
apply(plugin = "com.adarshr.test-logger")
|
|
||||||
apply(plugin = "idea")
|
|
||||||
|
|
||||||
tasks.withType<KotlinCompile>().configureEach {
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = "11"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
configure<TestLoggerExtension> {
|
|
||||||
theme = ThemeType.MOCHA
|
|
||||||
setLogLevel("lifecycle")
|
|
||||||
showExceptions = true
|
|
||||||
showStackTraces = true
|
|
||||||
showFullStackTraces = false
|
|
||||||
showCauses = true
|
|
||||||
slowThreshold = 2000
|
|
||||||
showSummary = true
|
|
||||||
showSimpleNames = false
|
|
||||||
showPassed = true
|
|
||||||
showSkipped = true
|
|
||||||
showFailed = true
|
|
||||||
showStandardStreams = false
|
|
||||||
showPassedStandardStreams = true
|
|
||||||
showSkippedStandardStreams = true
|
|
||||||
showFailedStandardStreams = true
|
|
||||||
}
|
|
||||||
|
|
||||||
configure<DetektExtension> {
|
|
||||||
toolVersion = "1.17.0-RC3"
|
|
||||||
config = files("${rootProject.projectDir}/detekt.yml")
|
|
||||||
buildUponDefaultConfig = true
|
|
||||||
}
|
|
||||||
|
|
||||||
configure<JavaPluginExtension> {
|
|
||||||
withSourcesJar()
|
|
||||||
withJavadocJar()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
nexusPublishing {
|
|
||||||
repositories {
|
|
||||||
sonatype {
|
|
||||||
nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/"))
|
|
||||||
snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
3
codacy.yml
Normal file
3
codacy.yml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
---
|
||||||
|
exclude_paths:
|
||||||
|
- "**/*.md"
|
705
detekt.yml
705
detekt.yml
@ -1,710 +1,25 @@
|
|||||||
build:
|
|
||||||
maxIssues: 0
|
|
||||||
excludeCorrectable: false
|
|
||||||
weights:
|
|
||||||
# complexity: 2
|
|
||||||
# LongParameterList: 1
|
|
||||||
# style: 1
|
|
||||||
# comments: 1
|
|
||||||
|
|
||||||
config:
|
|
||||||
validation: true
|
|
||||||
warningsAsErrors: false
|
|
||||||
# when writing own rules with new properties, exclude the property path e.g.: 'my_rule_set,.*>.*>[my_property]'
|
|
||||||
excludes: ''
|
|
||||||
|
|
||||||
processors:
|
|
||||||
active: true
|
|
||||||
exclude:
|
|
||||||
- 'DetektProgressListener'
|
|
||||||
|
|
||||||
console-reports:
|
|
||||||
active: true
|
|
||||||
exclude:
|
|
||||||
- 'ProjectStatisticsReport'
|
|
||||||
- 'ComplexityReport'
|
|
||||||
- 'NotificationReport'
|
|
||||||
- 'FileBasedFindingsReport'
|
|
||||||
|
|
||||||
output-reports:
|
|
||||||
active: true
|
|
||||||
|
|
||||||
comments:
|
|
||||||
active: true
|
|
||||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
|
||||||
AbsentOrWrongFileLicense:
|
|
||||||
active: false
|
|
||||||
licenseTemplateFile: 'license.template'
|
|
||||||
CommentOverPrivateFunction:
|
|
||||||
active: false
|
|
||||||
CommentOverPrivateProperty:
|
|
||||||
active: false
|
|
||||||
EndOfSentenceFormat:
|
|
||||||
active: false
|
|
||||||
endOfSentenceFormat: '([.?!][ \t\n\r\f<])|([.?!:]$)'
|
|
||||||
UndocumentedPublicClass:
|
|
||||||
active: false
|
|
||||||
searchInNestedClass: true
|
|
||||||
searchInInnerClass: true
|
|
||||||
searchInInnerObject: true
|
|
||||||
searchInInnerInterface: true
|
|
||||||
UndocumentedPublicFunction:
|
|
||||||
active: false
|
|
||||||
UndocumentedPublicProperty:
|
|
||||||
active: false
|
|
||||||
|
|
||||||
complexity:
|
complexity:
|
||||||
active: true
|
TooManyFunctions:
|
||||||
ComplexCondition:
|
|
||||||
active: true
|
|
||||||
threshold: 4
|
|
||||||
ComplexInterface:
|
|
||||||
active: false
|
active: false
|
||||||
threshold: 10
|
|
||||||
includeStaticDeclarations: false
|
|
||||||
includePrivateDeclarations: false
|
|
||||||
ComplexMethod:
|
|
||||||
active: true
|
|
||||||
threshold: 25
|
|
||||||
ignoreSingleWhenExpression: false
|
|
||||||
ignoreSimpleWhenEntries: false
|
|
||||||
ignoreNestingFunctions: false
|
|
||||||
nestingFunctions: [run, let, apply, with, also, use, forEach, isNotNull, ifNull]
|
|
||||||
LabeledExpression:
|
|
||||||
active: false
|
|
||||||
ignoredLabels: []
|
|
||||||
LargeClass:
|
|
||||||
active: true
|
|
||||||
threshold: 600
|
|
||||||
LongMethod:
|
|
||||||
active: true
|
|
||||||
threshold: 80
|
|
||||||
LongParameterList:
|
LongParameterList:
|
||||||
active: true
|
active: true
|
||||||
functionThreshold: 10
|
functionThreshold: 10
|
||||||
constructorThreshold: 10
|
constructorThreshold: 10
|
||||||
ignoreDefaultParameters: false
|
ComplexMethod:
|
||||||
ignoreDataClasses: true
|
threshold: 20
|
||||||
ignoreAnnotated: []
|
|
||||||
MethodOverloading:
|
|
||||||
active: false
|
|
||||||
threshold: 6
|
|
||||||
NamedArguments:
|
|
||||||
active: false
|
|
||||||
threshold: 3
|
|
||||||
NestedBlockDepth:
|
|
||||||
active: true
|
|
||||||
threshold: 6
|
|
||||||
ReplaceSafeCallChainWithRun:
|
|
||||||
active: false
|
|
||||||
StringLiteralDuplication:
|
|
||||||
active: false
|
|
||||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
|
||||||
threshold: 3
|
|
||||||
ignoreAnnotation: true
|
|
||||||
excludeStringsWithLessThan5Characters: true
|
|
||||||
ignoreStringsRegex: '$^'
|
|
||||||
TooManyFunctions:
|
|
||||||
active: false
|
|
||||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
|
||||||
thresholdInFiles: 11
|
|
||||||
thresholdInClasses: 11
|
|
||||||
thresholdInInterfaces: 11
|
|
||||||
thresholdInObjects: 11
|
|
||||||
thresholdInEnums: 11
|
|
||||||
ignoreDeprecated: false
|
|
||||||
ignorePrivate: false
|
|
||||||
ignoreOverridden: false
|
|
||||||
|
|
||||||
coroutines:
|
|
||||||
active: true
|
|
||||||
GlobalCoroutineUsage:
|
|
||||||
active: false
|
|
||||||
RedundantSuspendModifier:
|
|
||||||
active: false
|
|
||||||
SleepInsteadOfDelay:
|
|
||||||
active: false
|
|
||||||
SuspendFunWithFlowReturnType:
|
|
||||||
active: false
|
|
||||||
|
|
||||||
empty-blocks:
|
|
||||||
active: true
|
|
||||||
EmptyCatchBlock:
|
|
||||||
active: true
|
|
||||||
allowedExceptionNameRegex: '_|(ignore|expected).*'
|
|
||||||
EmptyClassBlock:
|
|
||||||
active: true
|
|
||||||
EmptyDefaultConstructor:
|
|
||||||
active: true
|
|
||||||
EmptyDoWhileBlock:
|
|
||||||
active: true
|
|
||||||
EmptyElseBlock:
|
|
||||||
active: true
|
|
||||||
EmptyFinallyBlock:
|
|
||||||
active: true
|
|
||||||
EmptyForBlock:
|
|
||||||
active: true
|
|
||||||
EmptyFunctionBlock:
|
|
||||||
active: true
|
|
||||||
ignoreOverridden: false
|
|
||||||
EmptyIfBlock:
|
|
||||||
active: true
|
|
||||||
EmptyInitBlock:
|
|
||||||
active: true
|
|
||||||
EmptyKtFile:
|
|
||||||
active: true
|
|
||||||
EmptySecondaryConstructor:
|
|
||||||
active: true
|
|
||||||
EmptyTryBlock:
|
|
||||||
active: true
|
|
||||||
EmptyWhenBlock:
|
|
||||||
active: true
|
|
||||||
EmptyWhileBlock:
|
|
||||||
active: true
|
|
||||||
|
|
||||||
exceptions:
|
|
||||||
active: true
|
|
||||||
ExceptionRaisedInUnexpectedLocation:
|
|
||||||
active: true
|
|
||||||
methodNames: [toString, hashCode, equals, finalize]
|
|
||||||
InstanceOfCheckForException:
|
|
||||||
active: false
|
|
||||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
|
||||||
NotImplementedDeclaration:
|
|
||||||
active: false
|
|
||||||
ObjectExtendsThrowable:
|
|
||||||
active: false
|
|
||||||
PrintStackTrace:
|
|
||||||
active: true
|
|
||||||
RethrowCaughtException:
|
|
||||||
active: true
|
|
||||||
ReturnFromFinally:
|
|
||||||
active: true
|
|
||||||
ignoreLabeled: false
|
|
||||||
SwallowedException:
|
|
||||||
active: true
|
|
||||||
ignoredExceptionTypes:
|
|
||||||
- InterruptedException
|
|
||||||
- NumberFormatException
|
|
||||||
- ParseException
|
|
||||||
- MalformedURLException
|
|
||||||
allowedExceptionNameRegex: '_|(ignore|expected).*'
|
|
||||||
ThrowingExceptionFromFinally:
|
|
||||||
active: true
|
|
||||||
ThrowingExceptionInMain:
|
|
||||||
active: false
|
|
||||||
ThrowingExceptionsWithoutMessageOrCause:
|
|
||||||
active: true
|
|
||||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
|
||||||
exceptions:
|
|
||||||
- IllegalArgumentException
|
|
||||||
- IllegalStateException
|
|
||||||
- IOException
|
|
||||||
ThrowingNewInstanceOfSameException:
|
|
||||||
active: true
|
|
||||||
TooGenericExceptionCaught:
|
|
||||||
active: true
|
|
||||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
|
||||||
exceptionNames:
|
|
||||||
- ArrayIndexOutOfBoundsException
|
|
||||||
- Error
|
|
||||||
- Exception
|
|
||||||
- IllegalMonitorStateException
|
|
||||||
- NullPointerException
|
|
||||||
- IndexOutOfBoundsException
|
|
||||||
- RuntimeException
|
|
||||||
- Throwable
|
|
||||||
allowedExceptionNameRegex: '_|(ignore|expected).*'
|
|
||||||
TooGenericExceptionThrown:
|
|
||||||
active: true
|
|
||||||
exceptionNames:
|
|
||||||
- Error
|
|
||||||
- Exception
|
|
||||||
- Throwable
|
|
||||||
- RuntimeException
|
|
||||||
|
|
||||||
formatting:
|
formatting:
|
||||||
active: true
|
|
||||||
android: false
|
|
||||||
autoCorrect: true
|
|
||||||
AnnotationOnSeparateLine:
|
|
||||||
active: false
|
|
||||||
autoCorrect: true
|
|
||||||
AnnotationSpacing:
|
|
||||||
active: false
|
|
||||||
autoCorrect: true
|
|
||||||
ArgumentListWrapping:
|
|
||||||
active: false
|
|
||||||
autoCorrect: true
|
|
||||||
ChainWrapping:
|
|
||||||
active: true
|
|
||||||
autoCorrect: true
|
|
||||||
CommentSpacing:
|
|
||||||
active: true
|
|
||||||
autoCorrect: true
|
|
||||||
EnumEntryNameCase:
|
|
||||||
active: false
|
|
||||||
autoCorrect: true
|
|
||||||
Filename:
|
|
||||||
active: true
|
|
||||||
FinalNewline:
|
|
||||||
active: true
|
|
||||||
autoCorrect: true
|
|
||||||
insertFinalNewLine: true
|
|
||||||
ImportOrdering:
|
|
||||||
active: false
|
|
||||||
autoCorrect: true
|
|
||||||
layout: 'idea'
|
|
||||||
Indentation:
|
|
||||||
active: false
|
|
||||||
autoCorrect: true
|
|
||||||
indentSize: 4
|
|
||||||
continuationIndentSize: 4
|
|
||||||
MaximumLineLength:
|
|
||||||
active: true
|
|
||||||
maxLineLength: 120
|
|
||||||
ModifierOrdering:
|
|
||||||
active: true
|
|
||||||
autoCorrect: true
|
|
||||||
MultiLineIfElse:
|
|
||||||
active: true
|
|
||||||
autoCorrect: true
|
|
||||||
NoBlankLineBeforeRbrace:
|
|
||||||
active: true
|
|
||||||
autoCorrect: true
|
|
||||||
NoConsecutiveBlankLines:
|
|
||||||
active: true
|
|
||||||
autoCorrect: true
|
|
||||||
NoEmptyClassBody:
|
|
||||||
active: true
|
|
||||||
autoCorrect: true
|
|
||||||
NoEmptyFirstLineInMethodBlock:
|
|
||||||
active: false
|
|
||||||
autoCorrect: true
|
|
||||||
NoLineBreakAfterElse:
|
|
||||||
active: true
|
|
||||||
autoCorrect: true
|
|
||||||
NoLineBreakBeforeAssignment:
|
|
||||||
active: true
|
|
||||||
autoCorrect: true
|
|
||||||
NoMultipleSpaces:
|
|
||||||
active: true
|
|
||||||
autoCorrect: true
|
|
||||||
NoSemicolons:
|
|
||||||
active: true
|
|
||||||
autoCorrect: true
|
|
||||||
NoTrailingSpaces:
|
|
||||||
active: true
|
|
||||||
autoCorrect: true
|
|
||||||
NoUnitReturn:
|
|
||||||
active: true
|
|
||||||
autoCorrect: true
|
|
||||||
NoUnusedImports:
|
|
||||||
active: true
|
|
||||||
autoCorrect: true
|
|
||||||
NoWildcardImports:
|
|
||||||
active: true
|
|
||||||
PackageName:
|
|
||||||
active: true
|
|
||||||
autoCorrect: true
|
|
||||||
ParameterListWrapping:
|
ParameterListWrapping:
|
||||||
active: false
|
active: false
|
||||||
autoCorrect: true
|
|
||||||
indentSize: 4
|
|
||||||
SpacingAroundAngleBrackets:
|
|
||||||
active: false
|
|
||||||
autoCorrect: true
|
|
||||||
SpacingAroundColon:
|
|
||||||
active: true
|
|
||||||
autoCorrect: true
|
|
||||||
SpacingAroundComma:
|
|
||||||
active: true
|
|
||||||
autoCorrect: true
|
|
||||||
SpacingAroundCurly:
|
|
||||||
active: true
|
|
||||||
autoCorrect: true
|
|
||||||
SpacingAroundDot:
|
|
||||||
active: true
|
|
||||||
autoCorrect: true
|
|
||||||
SpacingAroundDoubleColon:
|
|
||||||
active: false
|
|
||||||
autoCorrect: true
|
|
||||||
SpacingAroundKeyword:
|
|
||||||
active: true
|
|
||||||
autoCorrect: true
|
|
||||||
SpacingAroundOperators:
|
|
||||||
active: true
|
|
||||||
autoCorrect: true
|
|
||||||
SpacingAroundParens:
|
|
||||||
active: true
|
|
||||||
autoCorrect: true
|
|
||||||
SpacingAroundRangeOperator:
|
|
||||||
active: true
|
|
||||||
autoCorrect: true
|
|
||||||
SpacingAroundUnaryOperator:
|
|
||||||
active: false
|
|
||||||
autoCorrect: true
|
|
||||||
SpacingBetweenDeclarationsWithAnnotations:
|
|
||||||
active: false
|
|
||||||
autoCorrect: true
|
|
||||||
SpacingBetweenDeclarationsWithComments:
|
|
||||||
active: false
|
|
||||||
autoCorrect: true
|
|
||||||
StringTemplate:
|
|
||||||
active: true
|
|
||||||
autoCorrect: true
|
|
||||||
|
|
||||||
naming:
|
|
||||||
active: true
|
|
||||||
ClassNaming:
|
|
||||||
active: true
|
|
||||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
|
||||||
classPattern: '[A-Z][a-zA-Z0-9]*'
|
|
||||||
ConstructorParameterNaming:
|
|
||||||
active: false
|
|
||||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
|
||||||
parameterPattern: '[a-z][A-Za-z0-9]*'
|
|
||||||
privateParameterPattern: '[a-z][A-Za-z0-9]*'
|
|
||||||
excludeClassPattern: '$^'
|
|
||||||
ignoreOverridden: true
|
|
||||||
EnumNaming:
|
|
||||||
active: true
|
|
||||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
|
||||||
enumEntryPattern: '[A-Z][_a-zA-Z0-9]*'
|
|
||||||
ForbiddenClassName:
|
|
||||||
active: false
|
|
||||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
|
||||||
forbiddenName: []
|
|
||||||
FunctionMaxLength:
|
|
||||||
active: false
|
|
||||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
|
||||||
maximumFunctionNameLength: 30
|
|
||||||
FunctionMinLength:
|
|
||||||
active: false
|
|
||||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
|
||||||
minimumFunctionNameLength: 3
|
|
||||||
FunctionNaming:
|
|
||||||
active: true
|
|
||||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
|
||||||
functionPattern: '([a-z][a-zA-Z0-9]*)|(`.*`)'
|
|
||||||
excludeClassPattern: '$^'
|
|
||||||
ignoreOverridden: true
|
|
||||||
ignoreAnnotated: ['Composable']
|
|
||||||
FunctionParameterNaming:
|
|
||||||
active: true
|
|
||||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
|
||||||
parameterPattern: '[a-z][A-Za-z0-9]*'
|
|
||||||
excludeClassPattern: '$^'
|
|
||||||
ignoreOverridden: true
|
|
||||||
InvalidPackageDeclaration:
|
|
||||||
active: false
|
|
||||||
excludes: ['*.kts']
|
|
||||||
rootPackage: ''
|
|
||||||
MatchingDeclarationName:
|
|
||||||
active: true
|
|
||||||
mustBeFirst: true
|
|
||||||
MemberNameEqualsClassName:
|
|
||||||
active: true
|
|
||||||
ignoreOverridden: true
|
|
||||||
NonBooleanPropertyPrefixedWithIs:
|
|
||||||
active: false
|
|
||||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
|
||||||
ObjectPropertyNaming:
|
|
||||||
active: true
|
|
||||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
|
||||||
constantPattern: '[A-Za-z][_A-Za-z0-9]*'
|
|
||||||
propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
|
|
||||||
privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*'
|
|
||||||
PackageNaming:
|
|
||||||
active: true
|
|
||||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
|
||||||
packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*'
|
|
||||||
TopLevelPropertyNaming:
|
|
||||||
active: true
|
|
||||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
|
||||||
constantPattern: '[A-Z][_A-Z0-9]*'
|
|
||||||
propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
|
|
||||||
privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*'
|
|
||||||
VariableMaxLength:
|
|
||||||
active: false
|
|
||||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
|
||||||
maximumVariableNameLength: 64
|
|
||||||
VariableMinLength:
|
|
||||||
active: false
|
|
||||||
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
|
||||||
minimumVariableNameLength: 1
|
|
||||||
VariableNaming:
|
|
||||||
active: true
|
|
||||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
|
||||||
variablePattern: '[a-z][A-Za-z0-9]*'
|
|
||||||
privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*'
|
|
||||||
excludeClassPattern: '$^'
|
|
||||||
ignoreOverridden: true
|
|
||||||
|
|
||||||
performance:
|
|
||||||
active: true
|
|
||||||
ArrayPrimitive:
|
|
||||||
active: true
|
|
||||||
ForEachOnRange:
|
|
||||||
active: true
|
|
||||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
|
||||||
SpreadOperator:
|
|
||||||
active: true
|
|
||||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
|
||||||
UnnecessaryTemporaryInstantiation:
|
|
||||||
active: true
|
|
||||||
|
|
||||||
potential-bugs:
|
|
||||||
active: true
|
|
||||||
Deprecation:
|
|
||||||
active: false
|
|
||||||
DontDowncastCollectionTypes:
|
|
||||||
active: false
|
|
||||||
DuplicateCaseInWhenExpression:
|
|
||||||
active: true
|
|
||||||
EqualsAlwaysReturnsTrueOrFalse:
|
|
||||||
active: true
|
|
||||||
EqualsWithHashCodeExist:
|
|
||||||
active: true
|
|
||||||
ExitOutsideMain:
|
|
||||||
active: false
|
|
||||||
ExplicitGarbageCollectionCall:
|
|
||||||
active: true
|
|
||||||
HasPlatformType:
|
|
||||||
active: false
|
|
||||||
IgnoredReturnValue:
|
|
||||||
active: false
|
|
||||||
restrictToAnnotatedMethods: true
|
|
||||||
returnValueAnnotations: ['*.CheckReturnValue', '*.CheckResult']
|
|
||||||
ImplicitDefaultLocale:
|
|
||||||
active: true
|
|
||||||
ImplicitUnitReturnType:
|
|
||||||
active: false
|
|
||||||
allowExplicitReturnType: true
|
|
||||||
InvalidRange:
|
|
||||||
active: true
|
|
||||||
IteratorHasNextCallsNextMethod:
|
|
||||||
active: true
|
|
||||||
IteratorNotThrowingNoSuchElementException:
|
|
||||||
active: true
|
|
||||||
LateinitUsage:
|
|
||||||
active: false
|
|
||||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
|
||||||
excludeAnnotatedProperties: []
|
|
||||||
ignoreOnClassesPattern: ''
|
|
||||||
MapGetWithNotNullAssertionOperator:
|
|
||||||
active: false
|
|
||||||
MissingWhenCase:
|
|
||||||
active: true
|
|
||||||
allowElseExpression: true
|
|
||||||
NullableToStringCall:
|
|
||||||
active: false
|
|
||||||
RedundantElseInWhen:
|
|
||||||
active: true
|
|
||||||
UnconditionalJumpStatementInLoop:
|
|
||||||
active: false
|
|
||||||
UnnecessaryNotNullOperator:
|
|
||||||
active: true
|
|
||||||
UnnecessarySafeCall:
|
|
||||||
active: true
|
|
||||||
UnreachableCatchBlock:
|
|
||||||
active: false
|
|
||||||
UnreachableCode:
|
|
||||||
active: true
|
|
||||||
UnsafeCallOnNullableType:
|
|
||||||
active: true
|
|
||||||
UnsafeCast:
|
|
||||||
active: true
|
|
||||||
UselessPostfixExpression:
|
|
||||||
active: false
|
|
||||||
WrongEqualsTypeParameter:
|
|
||||||
active: true
|
|
||||||
|
|
||||||
style:
|
style:
|
||||||
active: true
|
|
||||||
ClassOrdering:
|
|
||||||
active: false
|
|
||||||
CollapsibleIfStatements:
|
|
||||||
active: false
|
|
||||||
DataClassContainsFunctions:
|
|
||||||
active: false
|
|
||||||
conversionFunctionPrefix: 'to'
|
|
||||||
DataClassShouldBeImmutable:
|
|
||||||
active: false
|
|
||||||
DestructuringDeclarationWithTooManyEntries:
|
|
||||||
active: false
|
|
||||||
maxDestructuringEntries: 3
|
|
||||||
EqualsNullCall:
|
|
||||||
active: true
|
|
||||||
EqualsOnSignatureLine:
|
|
||||||
active: false
|
|
||||||
ExplicitCollectionElementAccessMethod:
|
|
||||||
active: false
|
|
||||||
ExplicitItLambdaParameter:
|
|
||||||
active: false
|
|
||||||
ExpressionBodySyntax:
|
|
||||||
active: false
|
|
||||||
includeLineWrapping: false
|
|
||||||
ForbiddenComment:
|
|
||||||
active: true
|
|
||||||
values: ['TODO:', 'FIXME:', 'STOPSHIP:']
|
|
||||||
allowedPatterns: ''
|
|
||||||
ForbiddenImport:
|
|
||||||
active: false
|
|
||||||
imports: []
|
|
||||||
forbiddenPatterns: ''
|
|
||||||
ForbiddenMethodCall:
|
|
||||||
active: false
|
|
||||||
methods: ['kotlin.io.println', 'kotlin.io.print']
|
|
||||||
ForbiddenPublicDataClass:
|
|
||||||
active: true
|
|
||||||
excludes: ['**']
|
|
||||||
ignorePackages: ['*.internal', '*.internal.*']
|
|
||||||
ForbiddenVoid:
|
|
||||||
active: false
|
|
||||||
ignoreOverridden: false
|
|
||||||
ignoreUsageInGenerics: false
|
|
||||||
FunctionOnlyReturningConstant:
|
|
||||||
active: true
|
|
||||||
ignoreOverridableFunction: true
|
|
||||||
ignoreActualFunction: true
|
|
||||||
excludedFunctions: 'describeContents'
|
|
||||||
excludeAnnotatedFunction: ['dagger.Provides']
|
|
||||||
LibraryCodeMustSpecifyReturnType:
|
|
||||||
active: true
|
|
||||||
excludes: ['**']
|
|
||||||
LibraryEntitiesShouldNotBePublic:
|
|
||||||
active: true
|
|
||||||
excludes: ['**']
|
|
||||||
LoopWithTooManyJumpStatements:
|
|
||||||
active: true
|
|
||||||
maxJumpCount: 1
|
|
||||||
MagicNumber:
|
|
||||||
active: true
|
|
||||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
|
||||||
ignoreNumbers: ['-1', '0', '1', '2']
|
|
||||||
ignoreHashCodeFunction: true
|
|
||||||
ignorePropertyDeclaration: false
|
|
||||||
ignoreLocalVariableDeclaration: false
|
|
||||||
ignoreConstantDeclaration: true
|
|
||||||
ignoreCompanionObjectPropertyDeclaration: true
|
|
||||||
ignoreAnnotation: false
|
|
||||||
ignoreNamedArgument: true
|
|
||||||
ignoreEnums: false
|
|
||||||
ignoreRanges: false
|
|
||||||
ignoreExtensionFunctions: true
|
|
||||||
MandatoryBracesIfStatements:
|
|
||||||
active: false
|
|
||||||
MandatoryBracesLoops:
|
|
||||||
active: false
|
|
||||||
MaxLineLength:
|
MaxLineLength:
|
||||||
|
excludes: ['**/test/**/*']
|
||||||
active: true
|
active: true
|
||||||
maxLineLength: 120
|
maxLineLength: 120
|
||||||
excludePackageStatements: true
|
MagicNumber:
|
||||||
excludeImportStatements: true
|
excludes: ['**/kompendium-playground/**/*', '**/test/**/*']
|
||||||
excludeCommentStatements: false
|
naming:
|
||||||
MayBeConst:
|
ConstructorParameterNaming:
|
||||||
active: true
|
|
||||||
ModifierOrder:
|
|
||||||
active: true
|
|
||||||
MultilineLambdaItParameter:
|
|
||||||
active: false
|
active: false
|
||||||
NestedClassesVisibility:
|
performance:
|
||||||
active: true
|
SpreadOperator:
|
||||||
NewLineAtEndOfFile:
|
|
||||||
active: true
|
|
||||||
NoTabs:
|
|
||||||
active: false
|
active: false
|
||||||
OptionalAbstractKeyword:
|
|
||||||
active: true
|
|
||||||
OptionalUnit:
|
|
||||||
active: false
|
|
||||||
OptionalWhenBraces:
|
|
||||||
active: false
|
|
||||||
PreferToOverPairSyntax:
|
|
||||||
active: false
|
|
||||||
ProtectedMemberInFinalClass:
|
|
||||||
active: true
|
|
||||||
RedundantExplicitType:
|
|
||||||
active: false
|
|
||||||
RedundantHigherOrderMapUsage:
|
|
||||||
active: false
|
|
||||||
RedundantVisibilityModifierRule:
|
|
||||||
active: false
|
|
||||||
ReturnCount:
|
|
||||||
active: true
|
|
||||||
max: 2
|
|
||||||
excludedFunctions: 'equals'
|
|
||||||
excludeLabeled: false
|
|
||||||
excludeReturnFromLambda: true
|
|
||||||
excludeGuardClauses: false
|
|
||||||
SafeCast:
|
|
||||||
active: true
|
|
||||||
SerialVersionUIDInSerializableClass:
|
|
||||||
active: true
|
|
||||||
SpacingBetweenPackageAndImports:
|
|
||||||
active: false
|
|
||||||
ThrowsCount:
|
|
||||||
active: true
|
|
||||||
max: 2
|
|
||||||
TrailingWhitespace:
|
|
||||||
active: false
|
|
||||||
UnderscoresInNumericLiterals:
|
|
||||||
active: false
|
|
||||||
acceptableDecimalLength: 5
|
|
||||||
UnnecessaryAbstractClass:
|
|
||||||
active: true
|
|
||||||
excludeAnnotatedClasses: ['dagger.Module']
|
|
||||||
UnnecessaryAnnotationUseSiteTarget:
|
|
||||||
active: false
|
|
||||||
UnnecessaryApply:
|
|
||||||
active: true
|
|
||||||
UnnecessaryFilter:
|
|
||||||
active: false
|
|
||||||
UnnecessaryInheritance:
|
|
||||||
active: true
|
|
||||||
UnnecessaryLet:
|
|
||||||
active: false
|
|
||||||
UnnecessaryParentheses:
|
|
||||||
active: false
|
|
||||||
UntilInsteadOfRangeTo:
|
|
||||||
active: false
|
|
||||||
UnusedImports:
|
|
||||||
active: false
|
|
||||||
UnusedPrivateClass:
|
|
||||||
active: true
|
|
||||||
UnusedPrivateMember:
|
|
||||||
active: true
|
|
||||||
allowedNames: '(_|ignored|expected|serialVersionUID)'
|
|
||||||
UseArrayLiteralsInAnnotations:
|
|
||||||
active: false
|
|
||||||
UseCheckNotNull:
|
|
||||||
active: false
|
|
||||||
UseCheckOrError:
|
|
||||||
active: false
|
|
||||||
UseDataClass:
|
|
||||||
active: false
|
|
||||||
excludeAnnotatedClasses: []
|
|
||||||
allowVars: false
|
|
||||||
UseEmptyCounterpart:
|
|
||||||
active: false
|
|
||||||
UseIfEmptyOrIfBlank:
|
|
||||||
active: false
|
|
||||||
UseIfInsteadOfWhen:
|
|
||||||
active: false
|
|
||||||
UseIsNullOrEmpty:
|
|
||||||
active: false
|
|
||||||
UseRequire:
|
|
||||||
active: false
|
|
||||||
UseRequireNotNull:
|
|
||||||
active: false
|
|
||||||
UselessCallOnNotNull:
|
|
||||||
active: true
|
|
||||||
UtilityClassWithPublicConstructor:
|
|
||||||
active: true
|
|
||||||
VarCouldBeVal:
|
|
||||||
active: true
|
|
||||||
WildcardImport:
|
|
||||||
active: true
|
|
||||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
|
||||||
excludeImports: ['java.util.*', 'kotlinx.android.synthetic.*']
|
|
||||||
|
@ -1,7 +1,12 @@
|
|||||||
# Kompendium
|
# Kompendium
|
||||||
project.version=1.2.2
|
project.version=2.0.0-beta
|
||||||
# Kotlin
|
# Kotlin
|
||||||
kotlin.code.style=official
|
kotlin.code.style=official
|
||||||
# Gradle
|
# Gradle
|
||||||
org.gradle.vfs.watch=true
|
org.gradle.vfs.watch=true
|
||||||
org.gradle.vfs.verbose=true
|
org.gradle.vfs.verbose=true
|
||||||
|
org.gradle.jvmargs=-Xmx2000m
|
||||||
|
|
||||||
|
# Dependencies
|
||||||
|
ktorVersion=1.6.7
|
||||||
|
kotestVersion=5.0.3
|
||||||
|
@ -1,29 +0,0 @@
|
|||||||
[versions]
|
|
||||||
kotlin = "1.4.32"
|
|
||||||
ktor = "1.5.3"
|
|
||||||
slf4j = "1.7.30"
|
|
||||||
logback = "1.2.3"
|
|
||||||
swagger-ui = "3.47.1"
|
|
||||||
|
|
||||||
[libraries]
|
|
||||||
# API
|
|
||||||
ktor-server-core = { group = "io.ktor", name = "ktor-server-core", version.ref = "ktor" }
|
|
||||||
ktor-server-netty = { group = "io.ktor", name = "ktor-server-netty", version.ref = "ktor" }
|
|
||||||
ktor-jackson = { group = "io.ktor", name = "ktor-jackson", version.ref = "ktor" }
|
|
||||||
ktor-html-builder = { group = "io.ktor", name = "ktor-html-builder", version.ref = "ktor" }
|
|
||||||
ktor-auth-lib = { group = "io.ktor", name = "ktor-auth", version.ref = "ktor" }
|
|
||||||
ktor-auth-jwt = { group = "io.ktor", name = "ktor-auth-jwt", version.ref = "ktor" }
|
|
||||||
ktor-webjars = { group = "io.ktor", name = "ktor-webjars", version.ref = "ktor" }
|
|
||||||
|
|
||||||
# Logging
|
|
||||||
slf4j = { group = "org.slf4j", name = "slf4j-api", version.ref = "slf4j" }
|
|
||||||
logback-classic = { group = "ch.qos.logback", name = "logback-classic", version.ref = "logback" }
|
|
||||||
logback-core = { group = "ch.qos.logback", name = "logback-core", version.ref = "logback" }
|
|
||||||
|
|
||||||
# webjars
|
|
||||||
webjars-swagger-ui = { group = "org.webjars", name = "swagger-ui", version.ref = "swagger-ui" }
|
|
||||||
|
|
||||||
[bundles]
|
|
||||||
ktor = [ "ktor-server-core", "ktor-server-netty", "ktor-jackson", "ktor-html-builder" ]
|
|
||||||
ktorAuth = [ "ktor-auth-lib", "ktor-auth-jwt" ]
|
|
||||||
logging = [ "slf4j", "logback-classic", "logback-core" ]
|
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,5 +1,5 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
269
gradlew
vendored
269
gradlew
vendored
@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env sh
|
#!/bin/sh
|
||||||
|
|
||||||
#
|
#
|
||||||
# Copyright 2015 the original author or authors.
|
# Copyright © 2015-2021 the original authors.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@ -17,67 +17,101 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
##############################################################################
|
##############################################################################
|
||||||
##
|
#
|
||||||
## Gradle start up script for UN*X
|
# Gradle start up script for POSIX generated by Gradle.
|
||||||
##
|
#
|
||||||
|
# Important for running:
|
||||||
|
#
|
||||||
|
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||||
|
# noncompliant, but you have some other compliant shell such as ksh or
|
||||||
|
# bash, then to run this script, type that shell name before the whole
|
||||||
|
# command line, like:
|
||||||
|
#
|
||||||
|
# ksh Gradle
|
||||||
|
#
|
||||||
|
# Busybox and similar reduced shells will NOT work, because this script
|
||||||
|
# requires all of these POSIX shell features:
|
||||||
|
# * functions;
|
||||||
|
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||||
|
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||||
|
# * compound commands having a testable exit status, especially «case»;
|
||||||
|
# * various built-in commands including «command», «set», and «ulimit».
|
||||||
|
#
|
||||||
|
# Important for patching:
|
||||||
|
#
|
||||||
|
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||||
|
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||||
|
#
|
||||||
|
# The "traditional" practice of packing multiple parameters into a
|
||||||
|
# space-separated string is a well documented source of bugs and security
|
||||||
|
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||||
|
# options in "$@", and eventually passing that to Java.
|
||||||
|
#
|
||||||
|
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||||
|
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||||
|
# see the in-line comments for details.
|
||||||
|
#
|
||||||
|
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||||
|
# Darwin, MinGW, and NonStop.
|
||||||
|
#
|
||||||
|
# (3) This script is generated from the Groovy template
|
||||||
|
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
|
# within the Gradle project.
|
||||||
|
#
|
||||||
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
|
#
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
# Attempt to set APP_HOME
|
# Attempt to set APP_HOME
|
||||||
|
|
||||||
# Resolve links: $0 may be a link
|
# Resolve links: $0 may be a link
|
||||||
PRG="$0"
|
app_path=$0
|
||||||
# Need this for relative symlinks.
|
|
||||||
while [ -h "$PRG" ] ; do
|
# Need this for daisy-chained symlinks.
|
||||||
ls=`ls -ld "$PRG"`
|
while
|
||||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||||
if expr "$link" : '/.*' > /dev/null; then
|
[ -h "$app_path" ]
|
||||||
PRG="$link"
|
do
|
||||||
else
|
ls=$( ls -ld "$app_path" )
|
||||||
PRG=`dirname "$PRG"`"/$link"
|
link=${ls#*' -> '}
|
||||||
fi
|
case $link in #(
|
||||||
|
/*) app_path=$link ;; #(
|
||||||
|
*) app_path=$APP_HOME$link ;;
|
||||||
|
esac
|
||||||
done
|
done
|
||||||
SAVED="`pwd`"
|
|
||||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||||
APP_HOME="`pwd -P`"
|
|
||||||
cd "$SAVED" >/dev/null
|
|
||||||
|
|
||||||
APP_NAME="Gradle"
|
APP_NAME="Gradle"
|
||||||
APP_BASE_NAME=`basename "$0"`
|
APP_BASE_NAME=${0##*/}
|
||||||
|
|
||||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
MAX_FD="maximum"
|
MAX_FD=maximum
|
||||||
|
|
||||||
warn () {
|
warn () {
|
||||||
echo "$*"
|
echo "$*"
|
||||||
}
|
} >&2
|
||||||
|
|
||||||
die () {
|
die () {
|
||||||
echo
|
echo
|
||||||
echo "$*"
|
echo "$*"
|
||||||
echo
|
echo
|
||||||
exit 1
|
exit 1
|
||||||
}
|
} >&2
|
||||||
|
|
||||||
# OS specific support (must be 'true' or 'false').
|
# OS specific support (must be 'true' or 'false').
|
||||||
cygwin=false
|
cygwin=false
|
||||||
msys=false
|
msys=false
|
||||||
darwin=false
|
darwin=false
|
||||||
nonstop=false
|
nonstop=false
|
||||||
case "`uname`" in
|
case "$( uname )" in #(
|
||||||
CYGWIN* )
|
CYGWIN* ) cygwin=true ;; #(
|
||||||
cygwin=true
|
Darwin* ) darwin=true ;; #(
|
||||||
;;
|
MSYS* | MINGW* ) msys=true ;; #(
|
||||||
Darwin* )
|
NONSTOP* ) nonstop=true ;;
|
||||||
darwin=true
|
|
||||||
;;
|
|
||||||
MINGW* )
|
|
||||||
msys=true
|
|
||||||
;;
|
|
||||||
NONSTOP* )
|
|
||||||
nonstop=true
|
|
||||||
;;
|
|
||||||
esac
|
esac
|
||||||
|
|
||||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
@ -87,9 +121,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
|||||||
if [ -n "$JAVA_HOME" ] ; then
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
# IBM's JDK on AIX uses strange locations for the executables
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||||
else
|
else
|
||||||
JAVACMD="$JAVA_HOME/bin/java"
|
JAVACMD=$JAVA_HOME/bin/java
|
||||||
fi
|
fi
|
||||||
if [ ! -x "$JAVACMD" ] ; then
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
@ -98,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the
|
|||||||
location of your Java installation."
|
location of your Java installation."
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
JAVACMD="java"
|
JAVACMD=java
|
||||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
Please set the JAVA_HOME variable in your environment to match the
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
@ -106,80 +140,95 @@ location of your Java installation."
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Increase the maximum file descriptors if we can.
|
# Increase the maximum file descriptors if we can.
|
||||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
MAX_FD_LIMIT=`ulimit -H -n`
|
case $MAX_FD in #(
|
||||||
if [ $? -eq 0 ] ; then
|
max*)
|
||||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
MAX_FD="$MAX_FD_LIMIT"
|
warn "Could not query maximum file descriptor limit"
|
||||||
fi
|
esac
|
||||||
ulimit -n $MAX_FD
|
case $MAX_FD in #(
|
||||||
if [ $? -ne 0 ] ; then
|
'' | soft) :;; #(
|
||||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
*)
|
||||||
fi
|
ulimit -n "$MAX_FD" ||
|
||||||
else
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# For Darwin, add options to specify how the application appears in the dock
|
|
||||||
if $darwin; then
|
|
||||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
|
||||||
fi
|
|
||||||
|
|
||||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
|
||||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
|
||||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
|
||||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
|
||||||
|
|
||||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
|
||||||
|
|
||||||
# We build the pattern for arguments to be converted via cygpath
|
|
||||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
|
||||||
SEP=""
|
|
||||||
for dir in $ROOTDIRSRAW ; do
|
|
||||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
|
||||||
SEP="|"
|
|
||||||
done
|
|
||||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
|
||||||
# Add a user-defined pattern to the cygpath arguments
|
|
||||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
|
||||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
|
||||||
fi
|
|
||||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
|
||||||
i=0
|
|
||||||
for arg in "$@" ; do
|
|
||||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
|
||||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
|
||||||
|
|
||||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
|
||||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
|
||||||
else
|
|
||||||
eval `echo args$i`="\"$arg\""
|
|
||||||
fi
|
|
||||||
i=`expr $i + 1`
|
|
||||||
done
|
|
||||||
case $i in
|
|
||||||
0) set -- ;;
|
|
||||||
1) set -- "$args0" ;;
|
|
||||||
2) set -- "$args0" "$args1" ;;
|
|
||||||
3) set -- "$args0" "$args1" "$args2" ;;
|
|
||||||
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
|
||||||
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
|
||||||
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
|
||||||
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
|
||||||
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
|
||||||
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
|
||||||
esac
|
esac
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Escape application args
|
# Collect all arguments for the java command, stacking in reverse order:
|
||||||
save () {
|
# * args from the command line
|
||||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
# * the main class name
|
||||||
echo " "
|
# * -classpath
|
||||||
}
|
# * -D...appname settings
|
||||||
APP_ARGS=`save "$@"`
|
# * --module-path (only if needed)
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||||
|
|
||||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
if "$cygwin" || "$msys" ; then
|
||||||
|
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||||
|
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||||
|
|
||||||
|
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||||
|
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
for arg do
|
||||||
|
if
|
||||||
|
case $arg in #(
|
||||||
|
-*) false ;; # don't mess with options #(
|
||||||
|
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||||
|
[ -e "$t" ] ;; #(
|
||||||
|
*) false ;;
|
||||||
|
esac
|
||||||
|
then
|
||||||
|
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||||
|
fi
|
||||||
|
# Roll the args list around exactly as many times as the number of
|
||||||
|
# args, so each arg winds up back in the position where it started, but
|
||||||
|
# possibly modified.
|
||||||
|
#
|
||||||
|
# NB: a `for` loop captures its iteration list before it begins, so
|
||||||
|
# changing the positional parameters here affects neither the number of
|
||||||
|
# iterations, nor the values presented in `arg`.
|
||||||
|
shift # remove old arg
|
||||||
|
set -- "$@" "$arg" # push replacement arg
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Collect all arguments for the java command;
|
||||||
|
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||||
|
# shell script including quotes and variable substitutions, so put them in
|
||||||
|
# double quotes to make sure that they get re-expanded; and
|
||||||
|
# * put everything else in single quotes, so that it's not re-expanded.
|
||||||
|
|
||||||
|
set -- \
|
||||||
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
|
-classpath "$CLASSPATH" \
|
||||||
|
org.gradle.wrapper.GradleWrapperMain \
|
||||||
|
"$@"
|
||||||
|
|
||||||
|
# Use "xargs" to parse quoted args.
|
||||||
|
#
|
||||||
|
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||||
|
#
|
||||||
|
# In Bash we could simply go:
|
||||||
|
#
|
||||||
|
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||||
|
# set -- "${ARGS[@]}" "$@"
|
||||||
|
#
|
||||||
|
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||||
|
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||||
|
# character that might be a shell metacharacter, then use eval to reverse
|
||||||
|
# that process (while maintaining the separation between arguments), and wrap
|
||||||
|
# the whole thing up as a single "set" statement.
|
||||||
|
#
|
||||||
|
# This will of course break if any of these variables contains a newline or
|
||||||
|
# an unmatched quote.
|
||||||
|
#
|
||||||
|
|
||||||
|
eval "set -- $(
|
||||||
|
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||||
|
xargs -n1 |
|
||||||
|
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||||
|
tr '\n' ' '
|
||||||
|
)" '"$@"'
|
||||||
|
|
||||||
exec "$JAVACMD" "$@"
|
exec "$JAVACMD" "$@"
|
||||||
|
13
kompendium-annotations/Module.md
Normal file
13
kompendium-annotations/Module.md
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# Module kompendium-annotations
|
||||||
|
|
||||||
|
This module houses all annotations that Kompendium uses to provide key metadata when performing reflective analysis.
|
||||||
|
|
||||||
|
It is separated from core predominantly to allow for potential future integrations with [Kotlin Symbol Processing](https://github.com/google/ksp)
|
||||||
|
|
||||||
|
# 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.
|
30
kompendium-annotations/build.gradle.kts
Normal file
30
kompendium-annotations/build.gradle.kts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
plugins {
|
||||||
|
kotlin("jvm")
|
||||||
|
id("io.bkbn.sourdough.library.jvm") version "0.5.4"
|
||||||
|
id("io.gitlab.arturbosch.detekt") version "1.19.0"
|
||||||
|
id("com.adarshr.test-logger") version "3.1.0"
|
||||||
|
id("org.jetbrains.dokka")
|
||||||
|
id("maven-publish")
|
||||||
|
id("java-library")
|
||||||
|
id("signing")
|
||||||
|
}
|
||||||
|
|
||||||
|
sourdough {
|
||||||
|
githubOrg.set("bkbnio")
|
||||||
|
githubRepo.set("kompendium")
|
||||||
|
libraryName.set("Kompendium Annotations")
|
||||||
|
libraryDescription.set("A set of annotations used by Kompendium to generate OpenAPI Specifications")
|
||||||
|
licenseName.set("MIT License")
|
||||||
|
licenseUrl.set("https://mit-license.org")
|
||||||
|
developerId.set("unredundant")
|
||||||
|
developerName.set("Ryan Brink")
|
||||||
|
developerEmail.set("admin@bkbn.io")
|
||||||
|
}
|
||||||
|
|
||||||
|
testing {
|
||||||
|
suites {
|
||||||
|
named("test", JvmTestSuite::class) {
|
||||||
|
useJUnitJupiter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
package io.bkbn.kompendium.annotations
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Annotation used to perform field level overrides.
|
||||||
|
* @param name Indicates that a field name override is desired. Often used for camel case to snake case conversions.
|
||||||
|
*/
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
@Target(AnnotationTarget.PROPERTY)
|
||||||
|
annotation class Field(val name: String = "", val description: String = "")
|
@ -2,4 +2,4 @@ package io.bkbn.kompendium.annotations
|
|||||||
|
|
||||||
@Retention(AnnotationRetention.RUNTIME)
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
@Target(AnnotationTarget.PROPERTY)
|
@Target(AnnotationTarget.PROPERTY)
|
||||||
annotation class KompendiumField(val name: String)
|
annotation class FreeFormObject
|
@ -0,0 +1,9 @@
|
|||||||
|
package io.bkbn.kompendium.annotations
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to indicate that a field in a data class represents an OpenAPI parameter
|
||||||
|
* @param type The type of parameter, must be valid [ParamType]
|
||||||
|
*/
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
@Target(AnnotationTarget.PROPERTY)
|
||||||
|
annotation class Param(val type: ParamType)
|
@ -0,0 +1,11 @@
|
|||||||
|
package io.bkbn.kompendium.annotations
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The allowed parameter types as specified by the OpenAPI specification
|
||||||
|
*/
|
||||||
|
enum class ParamType {
|
||||||
|
COOKIE,
|
||||||
|
HEADER,
|
||||||
|
PATH,
|
||||||
|
QUERY
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
package io.bkbn.kompendium.annotations
|
||||||
|
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This annotation allows users to add additional fields that are not part of the core data model. This should be used
|
||||||
|
* EXTREMELY sparingly. Most useful in supporting a variety of polymorphic serialization techniques.
|
||||||
|
* @param field Name of the extra field to add to the model
|
||||||
|
* @param clazz Class type of the field being added. If this is a complex type, you are most likely doing something
|
||||||
|
* wrong.
|
||||||
|
*/
|
||||||
|
@Repeatable
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
@Target(AnnotationTarget.CLASS)
|
||||||
|
annotation class UndeclaredField(val field: String, val clazz: KClass<*>)
|
@ -0,0 +1,5 @@
|
|||||||
|
package io.bkbn.kompendium.annotations.constraint
|
||||||
|
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
@Target(AnnotationTarget.PROPERTY)
|
||||||
|
annotation class Format(val format: String)
|
@ -0,0 +1,5 @@
|
|||||||
|
package io.bkbn.kompendium.annotations.constraint
|
||||||
|
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
@Target(AnnotationTarget.PROPERTY)
|
||||||
|
annotation class MaxItems(val items: Int)
|
@ -0,0 +1,5 @@
|
|||||||
|
package io.bkbn.kompendium.annotations.constraint
|
||||||
|
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
@Target(AnnotationTarget.PROPERTY)
|
||||||
|
annotation class MaxLength(val length: Int)
|
@ -0,0 +1,5 @@
|
|||||||
|
package io.bkbn.kompendium.annotations.constraint
|
||||||
|
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
@Target(AnnotationTarget.PROPERTY)
|
||||||
|
annotation class MaxProperties(val properties: Int)
|
@ -0,0 +1,5 @@
|
|||||||
|
package io.bkbn.kompendium.annotations.constraint
|
||||||
|
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
@Target(AnnotationTarget.PROPERTY)
|
||||||
|
annotation class Maximum(val max: String, val exclusive: Boolean = false)
|
@ -0,0 +1,5 @@
|
|||||||
|
package io.bkbn.kompendium.annotations.constraint
|
||||||
|
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
@Target(AnnotationTarget.PROPERTY)
|
||||||
|
annotation class MinItems(val items: Int)
|
@ -0,0 +1,5 @@
|
|||||||
|
package io.bkbn.kompendium.annotations.constraint
|
||||||
|
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
@Target(AnnotationTarget.PROPERTY)
|
||||||
|
annotation class MinLength(val length: Int)
|
@ -0,0 +1,5 @@
|
|||||||
|
package io.bkbn.kompendium.annotations.constraint
|
||||||
|
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
@Target(AnnotationTarget.PROPERTY)
|
||||||
|
annotation class MinProperties(val properties: Int)
|
@ -0,0 +1,5 @@
|
|||||||
|
package io.bkbn.kompendium.annotations.constraint
|
||||||
|
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
@Target(AnnotationTarget.PROPERTY)
|
||||||
|
annotation class Minimum(val min: String, val exclusive: Boolean = false)
|
@ -0,0 +1,5 @@
|
|||||||
|
package io.bkbn.kompendium.annotations.constraint
|
||||||
|
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
@Target(AnnotationTarget.PROPERTY)
|
||||||
|
annotation class MultipleOf(val multiple: String)
|
@ -0,0 +1,5 @@
|
|||||||
|
package io.bkbn.kompendium.annotations.constraint
|
||||||
|
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
@Target(AnnotationTarget.PROPERTY)
|
||||||
|
annotation class Pattern(val pattern: String)
|
@ -0,0 +1,5 @@
|
|||||||
|
package io.bkbn.kompendium.annotations.constraint
|
||||||
|
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
@Target(AnnotationTarget.PROPERTY)
|
||||||
|
annotation class UniqueItems
|
13
kompendium-auth/Module.md
Normal file
13
kompendium-auth/Module.md
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# Module kompendium-auth
|
||||||
|
|
||||||
|
This module is responsible for providing wrappers around ktor-auth configuration blocks, allowing users to document
|
||||||
|
their API authentication with minimal modifications to their existing configuration.
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
# Package io.bkbn.kompendium.auth
|
||||||
|
|
||||||
|
Houses the available security configurations. At the moment, `Basic`, `JWT`, `ApiKey`, and `OAuth` are supported
|
@ -1,77 +1,44 @@
|
|||||||
plugins {
|
plugins {
|
||||||
`java-library`
|
kotlin("jvm")
|
||||||
`maven-publish`
|
id("io.bkbn.sourdough.library.jvm") version "0.5.4"
|
||||||
signing
|
id("io.gitlab.arturbosch.detekt") version "1.19.0"
|
||||||
|
id("com.adarshr.test-logger") version "3.1.0"
|
||||||
|
id("org.jetbrains.dokka")
|
||||||
|
id("maven-publish")
|
||||||
|
id("java-library")
|
||||||
|
id("signing")
|
||||||
|
}
|
||||||
|
|
||||||
|
sourdough {
|
||||||
|
githubOrg.set("bkbnio")
|
||||||
|
githubRepo.set("kompendium")
|
||||||
|
libraryName.set("Kompendium Authentication")
|
||||||
|
libraryDescription.set("Kompendium library to pair with Ktor Auth to provide authorization info to OpenAPI")
|
||||||
|
licenseName.set("MIT License")
|
||||||
|
licenseUrl.set("https://mit-license.org")
|
||||||
|
developerId.set("unredundant")
|
||||||
|
developerName.set("Ryan Brink")
|
||||||
|
developerEmail.set("admin@bkbn.io")
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(platform("org.jetbrains.kotlin:kotlin-bom"))
|
// IMPLEMENTATION
|
||||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
|
|
||||||
implementation(libs.bundles.ktor)
|
val ktorVersion: String by project
|
||||||
implementation(libs.bundles.ktorAuth)
|
|
||||||
implementation(projects.kompendiumCore)
|
implementation(projects.kompendiumCore)
|
||||||
|
implementation(group = "io.ktor", name = "ktor-server-core", version = ktorVersion)
|
||||||
|
implementation(group = "io.ktor", name = "ktor-auth", version = ktorVersion)
|
||||||
|
implementation(group = "io.ktor", name = "ktor-auth-jwt", version = ktorVersion)
|
||||||
|
|
||||||
testImplementation("org.jetbrains.kotlin:kotlin-test")
|
// TESTING
|
||||||
testImplementation("org.jetbrains.kotlin:kotlin-test-junit")
|
|
||||||
testImplementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.12.0")
|
testImplementation(testFixtures(projects.kompendiumCore))
|
||||||
testImplementation("io.ktor:ktor-server-test-host:1.5.3")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
java {
|
testing {
|
||||||
withSourcesJar()
|
suites {
|
||||||
withJavadocJar()
|
named("test", JvmTestSuite::class) {
|
||||||
}
|
useJUnitJupiter()
|
||||||
|
|
||||||
publishing {
|
|
||||||
repositories {
|
|
||||||
maven {
|
|
||||||
name = "GithubPackages"
|
|
||||||
url = uri("https://maven.pkg.github.com/bkbnio/kompendium")
|
|
||||||
credentials {
|
|
||||||
username = System.getenv("GITHUB_ACTOR")
|
|
||||||
password = System.getenv("GITHUB_TOKEN")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
publications {
|
|
||||||
create<MavenPublication>("kompendium") {
|
|
||||||
from(components["kotlin"])
|
|
||||||
artifact(tasks.sourcesJar)
|
|
||||||
artifact(tasks.javadocJar)
|
|
||||||
groupId = project.group.toString()
|
|
||||||
artifactId = project.name.toLowerCase()
|
|
||||||
version = project.version.toString()
|
|
||||||
|
|
||||||
pom {
|
|
||||||
name.set("Kompendium")
|
|
||||||
description.set("A minimally invasive OpenAPI spec generator for Ktor")
|
|
||||||
url.set("https://github.com/bkbnio/Kompendium")
|
|
||||||
licenses {
|
|
||||||
license {
|
|
||||||
name.set("MIT License")
|
|
||||||
url.set("https://mit-license.org/")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
developers {
|
|
||||||
developer {
|
|
||||||
id.set("bkbnio")
|
|
||||||
name.set("Ryan Brink")
|
|
||||||
email.set("admin@bkbn.io")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
scm {
|
|
||||||
connection.set("scm:git:git://github.com/bkbnio/Kompendium.git")
|
|
||||||
developerConnection.set("scm:git:ssh://github.com/bkbnio/Kompendium.git")
|
|
||||||
url.set("https://github.com/bkbnio/Kompendium.git")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
signing {
|
|
||||||
val signingKey: String? by project
|
|
||||||
val signingPassword: String? by project
|
|
||||||
useInMemoryPgpKeys(signingKey, signingPassword)
|
|
||||||
sign(publishing.publications)
|
|
||||||
}
|
|
||||||
|
@ -1,19 +0,0 @@
|
|||||||
package io.bkbn.kompendium.auth
|
|
||||||
|
|
||||||
import io.ktor.auth.AuthenticationRouteSelector
|
|
||||||
import io.ktor.routing.Route
|
|
||||||
import io.bkbn.kompendium.path.CorePathCalculator
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
|
|
||||||
class AuthPathCalculator : CorePathCalculator() {
|
|
||||||
|
|
||||||
private val logger = LoggerFactory.getLogger(javaClass)
|
|
||||||
|
|
||||||
override fun handleCustomSelectors(route: Route, tail: String): String = when (route.selector) {
|
|
||||||
is AuthenticationRouteSelector -> {
|
|
||||||
logger.debug("Found authentication route selector ${route.selector}")
|
|
||||||
super.calculate(route.parent, tail)
|
|
||||||
}
|
|
||||||
else -> super.handleCustomSelectors(route, tail)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,50 +0,0 @@
|
|||||||
package io.bkbn.kompendium.auth
|
|
||||||
|
|
||||||
import io.ktor.auth.Authentication
|
|
||||||
import io.ktor.auth.basic
|
|
||||||
import io.ktor.auth.BasicAuthenticationProvider
|
|
||||||
import io.ktor.auth.jwt.jwt
|
|
||||||
import io.ktor.auth.jwt.JWTAuthenticationProvider
|
|
||||||
import io.bkbn.kompendium.Kompendium
|
|
||||||
import io.bkbn.kompendium.models.oas.OpenApiSpecSchemaSecurity
|
|
||||||
|
|
||||||
object KompendiumAuth {
|
|
||||||
|
|
||||||
init {
|
|
||||||
Kompendium.pathCalculator = AuthPathCalculator()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Authentication.Configuration.notarizedBasic(
|
|
||||||
name: String? = null,
|
|
||||||
configure: BasicAuthenticationProvider.Configuration.() -> Unit
|
|
||||||
) {
|
|
||||||
Kompendium.openApiSpec.components.securitySchemes[name ?: "default"] = OpenApiSpecSchemaSecurity(
|
|
||||||
type = "http",
|
|
||||||
scheme = "basic"
|
|
||||||
)
|
|
||||||
basic(name, configure)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Authentication.Configuration.notarizedJwt(
|
|
||||||
name: String? = null,
|
|
||||||
header: String? = null,
|
|
||||||
scheme: String? = null,
|
|
||||||
configure: JWTAuthenticationProvider.Configuration.() -> Unit
|
|
||||||
) {
|
|
||||||
if (header == null || header == "Authorization") {
|
|
||||||
Kompendium.openApiSpec.components.securitySchemes[name ?: "default"] = OpenApiSpecSchemaSecurity(
|
|
||||||
type = "http",
|
|
||||||
scheme = scheme ?: "bearer"
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Kompendium.openApiSpec.components.securitySchemes[name ?: "default"] = OpenApiSpecSchemaSecurity(
|
|
||||||
type = "apiKey",
|
|
||||||
name = header,
|
|
||||||
`in` = "header"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
jwt(name, configure)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO support other authentication providers (e.g., oAuth)?
|
|
||||||
}
|
|
@ -0,0 +1,40 @@
|
|||||||
|
package io.bkbn.kompendium.auth
|
||||||
|
|
||||||
|
import io.bkbn.kompendium.auth.configuration.ApiKeyConfiguration
|
||||||
|
import io.bkbn.kompendium.auth.configuration.BasicAuthConfiguration
|
||||||
|
import io.bkbn.kompendium.auth.configuration.JwtAuthConfiguration
|
||||||
|
import io.bkbn.kompendium.auth.configuration.OAuthConfiguration
|
||||||
|
import io.bkbn.kompendium.auth.configuration.SecurityConfiguration
|
||||||
|
import io.bkbn.kompendium.core.Kompendium
|
||||||
|
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.ktor.application.feature
|
||||||
|
import io.ktor.auth.authenticate
|
||||||
|
import io.ktor.routing.Route
|
||||||
|
import io.ktor.routing.application
|
||||||
|
|
||||||
|
object Notarized {
|
||||||
|
|
||||||
|
fun Route.notarizedAuthenticate(
|
||||||
|
vararg configurations: SecurityConfiguration,
|
||||||
|
optional: Boolean = false,
|
||||||
|
build: Route.() -> Unit
|
||||||
|
): Route {
|
||||||
|
val configurationNames = configurations.map { it.name }.toTypedArray()
|
||||||
|
val feature = application.feature(Kompendium)
|
||||||
|
|
||||||
|
configurations.forEach { config ->
|
||||||
|
feature.config.spec.components.securitySchemes[config.name] = when (config) {
|
||||||
|
is ApiKeyConfiguration -> ApiKeyAuth(config.location, config.keyName)
|
||||||
|
is BasicAuthConfiguration -> BasicAuth()
|
||||||
|
is JwtAuthConfiguration -> BearerAuth(config.bearerFormat)
|
||||||
|
is OAuthConfiguration -> OAuth(config.description, config.flows)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return authenticate(*configurationNames, optional = optional, build = build)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
package io.bkbn.kompendium.auth.configuration
|
||||||
|
|
||||||
|
import io.bkbn.kompendium.oas.security.ApiKeyAuth
|
||||||
|
|
||||||
|
interface ApiKeyConfiguration : SecurityConfiguration {
|
||||||
|
val location: ApiKeyAuth.ApiKeyLocation
|
||||||
|
val keyName: String
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
package io.bkbn.kompendium.auth.configuration
|
||||||
|
|
||||||
|
interface BasicAuthConfiguration : SecurityConfiguration
|
@ -0,0 +1,6 @@
|
|||||||
|
package io.bkbn.kompendium.auth.configuration
|
||||||
|
|
||||||
|
interface JwtAuthConfiguration : SecurityConfiguration {
|
||||||
|
val bearerFormat: String
|
||||||
|
get() = "JWT"
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
package io.bkbn.kompendium.auth.configuration
|
||||||
|
|
||||||
|
import io.bkbn.kompendium.oas.security.OAuth
|
||||||
|
|
||||||
|
interface OAuthConfiguration: SecurityConfiguration {
|
||||||
|
val flows: OAuth.Flows
|
||||||
|
val description: String?
|
||||||
|
get() = null
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
package io.bkbn.kompendium.auth.configuration
|
||||||
|
|
||||||
|
sealed interface SecurityConfiguration {
|
||||||
|
val name: String
|
||||||
|
}
|
@ -1,201 +1,69 @@
|
|||||||
package io.bkbn.kompendium.auth
|
package io.bkbn.kompendium.auth
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonInclude
|
import io.bkbn.kompendium.auth.configuration.BasicAuthConfiguration
|
||||||
import com.fasterxml.jackson.databind.SerializationFeature
|
import io.bkbn.kompendium.auth.configuration.JwtAuthConfiguration
|
||||||
import io.ktor.application.Application
|
import io.bkbn.kompendium.auth.configuration.OAuthConfiguration
|
||||||
import io.ktor.application.call
|
import io.bkbn.kompendium.auth.util.AuthConfigName
|
||||||
import io.ktor.application.install
|
import io.bkbn.kompendium.auth.util.configBasicAuth
|
||||||
import io.ktor.auth.Authentication
|
import io.bkbn.kompendium.auth.util.configJwtAuth
|
||||||
import io.ktor.auth.UserIdPrincipal
|
import io.bkbn.kompendium.auth.util.notarizedAuthRoute
|
||||||
import io.ktor.auth.authenticate
|
import io.bkbn.kompendium.auth.util.setupOauth
|
||||||
import io.ktor.features.ContentNegotiation
|
import io.bkbn.kompendium.core.fixtures.TestHelpers.openApiTest
|
||||||
import io.ktor.http.ContentType
|
import io.bkbn.kompendium.oas.security.OAuth
|
||||||
import io.ktor.http.HttpMethod
|
import io.kotest.core.spec.style.DescribeSpec
|
||||||
import io.ktor.http.HttpStatusCode
|
|
||||||
import io.ktor.jackson.jackson
|
|
||||||
import io.ktor.response.respondText
|
|
||||||
import io.ktor.routing.route
|
|
||||||
import io.ktor.routing.routing
|
|
||||||
import io.ktor.server.testing.handleRequest
|
|
||||||
import io.ktor.server.testing.withTestApplication
|
|
||||||
import kotlin.test.AfterTest
|
|
||||||
import kotlin.test.assertEquals
|
|
||||||
import org.junit.Test
|
|
||||||
import io.bkbn.kompendium.Kompendium
|
|
||||||
import io.bkbn.kompendium.Notarized.notarizedGet
|
|
||||||
import io.bkbn.kompendium.auth.KompendiumAuth.notarizedBasic
|
|
||||||
import io.bkbn.kompendium.auth.KompendiumAuth.notarizedJwt
|
|
||||||
import io.bkbn.kompendium.auth.util.TestData
|
|
||||||
import io.bkbn.kompendium.auth.util.TestParams
|
|
||||||
import io.bkbn.kompendium.auth.util.TestResponse
|
|
||||||
import io.bkbn.kompendium.models.meta.MethodInfo
|
|
||||||
import io.bkbn.kompendium.models.meta.ResponseInfo
|
|
||||||
import io.bkbn.kompendium.routes.openApi
|
|
||||||
import io.bkbn.kompendium.routes.redoc
|
|
||||||
|
|
||||||
internal class KompendiumAuthTest {
|
class KompendiumAuthTest : DescribeSpec({
|
||||||
|
describe("Basic Authentication") {
|
||||||
@AfterTest
|
it("Can create a notarized basic authentication record with all expected information") {
|
||||||
fun `reset kompendium`() {
|
// arrange
|
||||||
Kompendium.resetSchema()
|
val authConfig = object : BasicAuthConfiguration {
|
||||||
}
|
override val name: String = AuthConfigName.Basic
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `Notarized Get with basic authentication records all expected information`() {
|
|
||||||
withTestApplication({
|
|
||||||
configModule()
|
|
||||||
configBasicAuth()
|
|
||||||
docs()
|
|
||||||
notarizedAuthenticatedGetModule(TestData.AuthConfigName.Basic)
|
|
||||||
}) {
|
|
||||||
// do
|
|
||||||
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
|
|
||||||
|
|
||||||
// expect
|
|
||||||
val expected = TestData.getFileSnapshot("notarized_basic_authenticated_get.json").trim()
|
|
||||||
assertEquals(expected, json, "The received json spec should match the expected content")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `Notarized Get with jwt authentication records all expected information`() {
|
|
||||||
withTestApplication({
|
|
||||||
configModule()
|
|
||||||
configJwtAuth()
|
|
||||||
docs()
|
|
||||||
notarizedAuthenticatedGetModule(TestData.AuthConfigName.JWT)
|
|
||||||
}) {
|
|
||||||
// do
|
|
||||||
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
|
|
||||||
|
|
||||||
// expect
|
|
||||||
val expected = TestData.getFileSnapshot("notarized_jwt_authenticated_get.json").trim()
|
|
||||||
assertEquals(expected, json, "The received json spec should match the expected content")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `Notarized Get with jwt authentication and custom scheme records all expected information`() {
|
|
||||||
withTestApplication({
|
|
||||||
configModule()
|
|
||||||
configJwtAuth(scheme = "oauth")
|
|
||||||
docs()
|
|
||||||
notarizedAuthenticatedGetModule(TestData.AuthConfigName.JWT)
|
|
||||||
}) {
|
|
||||||
// do
|
|
||||||
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
|
|
||||||
|
|
||||||
// expect
|
|
||||||
val expected = TestData.getFileSnapshot("notarized_jwt_custom_scheme_authenticated_get.json").trim()
|
|
||||||
assertEquals(expected, json, "The received json spec should match the expected content")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `Notarized Get with jwt authentication and custom header records all expected information`() {
|
|
||||||
withTestApplication({
|
|
||||||
configModule()
|
|
||||||
configJwtAuth(header = "x-api-key")
|
|
||||||
docs()
|
|
||||||
notarizedAuthenticatedGetModule(TestData.AuthConfigName.JWT)
|
|
||||||
}) {
|
|
||||||
// do
|
|
||||||
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
|
|
||||||
|
|
||||||
// expect
|
|
||||||
val expected = TestData.getFileSnapshot("notarized_jwt_custom_header_authenticated_get.json").trim()
|
|
||||||
assertEquals(expected, json, "The received json spec should match the expected content")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `Notarized Get with multiple jwt schemes records all expected information`() {
|
|
||||||
withTestApplication({
|
|
||||||
configModule()
|
|
||||||
install(Authentication) {
|
|
||||||
notarizedJwt("jwt1", header = "x-api-key-1") {
|
|
||||||
realm = "Ktor server"
|
|
||||||
}
|
|
||||||
notarizedJwt("jwt2", header = "x-api-key-2") {
|
|
||||||
realm = "Ktor server"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
docs()
|
|
||||||
notarizedAuthenticatedGetModule("jwt1", "jwt2")
|
|
||||||
}) {
|
|
||||||
// do
|
|
||||||
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
|
|
||||||
|
|
||||||
// expect
|
// act
|
||||||
val expected = TestData.getFileSnapshot("notarized_multiple_jwt_authenticated_get.json").trim()
|
openApiTest("notarized_basic_authenticated_get.json") {
|
||||||
assertEquals(expected, json, "The received json spec should match the expected content")
|
configBasicAuth()
|
||||||
}
|
notarizedAuthRoute(authConfig)
|
||||||
}
|
|
||||||
|
|
||||||
private fun Application.configModule() {
|
|
||||||
install(ContentNegotiation) {
|
|
||||||
jackson(ContentType.Application.Json) {
|
|
||||||
enable(SerializationFeature.INDENT_OUTPUT)
|
|
||||||
setSerializationInclusion(JsonInclude.Include.NON_NULL)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
describe("JWT Authentication") {
|
||||||
|
it("Can create a simple notarized JWT route") {
|
||||||
|
// arrange
|
||||||
|
val authConfig = object : JwtAuthConfiguration {
|
||||||
|
override val name: String = AuthConfigName.JWT
|
||||||
|
}
|
||||||
|
|
||||||
private fun Application.configBasicAuth() {
|
// act
|
||||||
install(Authentication) {
|
openApiTest("notarized_jwt_authenticated_get.json") {
|
||||||
notarizedBasic(TestData.AuthConfigName.Basic) {
|
configJwtAuth()
|
||||||
realm = "Ktor Server"
|
notarizedAuthRoute(authConfig)
|
||||||
validate { credentials ->
|
|
||||||
if (credentials.name == credentials.password) {
|
|
||||||
UserIdPrincipal(credentials.name)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
describe("OAuth Authentication") {
|
||||||
private fun Application.configJwtAuth(
|
it("Can create an Oauth schema with all possible flows") {
|
||||||
header: String? = null,
|
// arrange
|
||||||
scheme: String? = null
|
val flows = OAuth.Flows(
|
||||||
) {
|
implicit = OAuth.Flows.Implicit(
|
||||||
install(Authentication) {
|
"https://accounts.google.com/o/oauth2/auth",
|
||||||
notarizedJwt(TestData.AuthConfigName.JWT, header, scheme) {
|
scopes = mapOf("test" to "is a cool scope", "this" to "is also cool")
|
||||||
realm = "Ktor server"
|
),
|
||||||
}
|
authorizationCode = OAuth.Flows.AuthorizationCode("https://accounts.google.com/o/oauth2/auth"),
|
||||||
}
|
password = OAuth.Flows.Password("https://accounts.google.com/o/oauth2/auth"),
|
||||||
}
|
clientCredentials = OAuth.Flows.ClientCredential("https://accounts.google.com/token")
|
||||||
|
|
||||||
private fun Application.notarizedAuthenticatedGetModule(vararg authenticationConfigName: String) {
|
|
||||||
routing {
|
|
||||||
authenticate(*authenticationConfigName) {
|
|
||||||
route(TestData.getRoutePath) {
|
|
||||||
notarizedGet(testGetInfo(*authenticationConfigName)) {
|
|
||||||
call.respondText { "hey dude ‼️ congratz on the get request" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val oas = Kompendium.openApiSpec.copy()
|
|
||||||
|
|
||||||
private fun Application.docs() {
|
|
||||||
routing {
|
|
||||||
openApi(oas)
|
|
||||||
redoc(oas)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private companion object {
|
|
||||||
val testGetResponse = ResponseInfo<TestResponse>(HttpStatusCode.OK, "A Successful Endeavor")
|
|
||||||
fun testGetInfo(vararg security: String) =
|
|
||||||
MethodInfo.GetInfo<TestParams, TestResponse>(
|
|
||||||
summary = "Another get test",
|
|
||||||
description = "testing more",
|
|
||||||
responseInfo = testGetResponse,
|
|
||||||
securitySchemes = security.toSet()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val authConfig = object : OAuthConfiguration {
|
||||||
|
override val flows: OAuth.Flows = flows
|
||||||
|
override val name: String = AuthConfigName.OAuth
|
||||||
|
}
|
||||||
|
|
||||||
|
// act
|
||||||
|
openApiTest("notarized_oauth_all_flows.json") {
|
||||||
|
setupOauth()
|
||||||
|
notarizedAuthRoute(authConfig)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
@ -1,18 +0,0 @@
|
|||||||
package io.bkbn.kompendium.auth.util
|
|
||||||
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
object TestData {
|
|
||||||
object AuthConfigName {
|
|
||||||
const val Basic = "basic"
|
|
||||||
const val JWT = "jwt"
|
|
||||||
}
|
|
||||||
|
|
||||||
const val getRoutePath = "/test"
|
|
||||||
|
|
||||||
fun getFileSnapshot(fileName: String): String {
|
|
||||||
val snapshotPath = "src/test/resources"
|
|
||||||
val file = File("$snapshotPath/$fileName")
|
|
||||||
return file.readText()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
package io.bkbn.kompendium.auth.util
|
|
||||||
|
|
||||||
import io.bkbn.kompendium.annotations.KompendiumField
|
|
||||||
import io.bkbn.kompendium.annotations.KompendiumParam
|
|
||||||
import io.bkbn.kompendium.annotations.ParamType
|
|
||||||
|
|
||||||
data class TestParams(
|
|
||||||
@KompendiumParam(ParamType.PATH) val a: String,
|
|
||||||
@KompendiumParam(ParamType.QUERY) val aa: Int
|
|
||||||
)
|
|
||||||
|
|
||||||
data class TestRequest(
|
|
||||||
@KompendiumField(name = "field_name")
|
|
||||||
val b: Double,
|
|
||||||
val aaa: List<Long>
|
|
||||||
)
|
|
||||||
|
|
||||||
data class TestResponse(val c: String)
|
|
||||||
|
|
@ -0,0 +1,92 @@
|
|||||||
|
package io.bkbn.kompendium.auth.util
|
||||||
|
|
||||||
|
import io.bkbn.kompendium.auth.Notarized.notarizedAuthenticate
|
||||||
|
import io.bkbn.kompendium.auth.configuration.SecurityConfiguration
|
||||||
|
import io.bkbn.kompendium.core.Notarized.notarizedGet
|
||||||
|
import io.bkbn.kompendium.core.fixtures.TestParams
|
||||||
|
import io.bkbn.kompendium.core.fixtures.TestResponse
|
||||||
|
import io.bkbn.kompendium.core.fixtures.TestResponseInfo
|
||||||
|
import io.bkbn.kompendium.core.metadata.method.GetInfo
|
||||||
|
import io.ktor.application.Application
|
||||||
|
import io.ktor.application.call
|
||||||
|
import io.ktor.application.install
|
||||||
|
import io.ktor.auth.Authentication
|
||||||
|
import io.ktor.auth.OAuthServerSettings
|
||||||
|
import io.ktor.auth.UserIdPrincipal
|
||||||
|
import io.ktor.auth.basic
|
||||||
|
import io.ktor.auth.jwt.jwt
|
||||||
|
import io.ktor.auth.oauth
|
||||||
|
import io.ktor.client.HttpClient
|
||||||
|
import io.ktor.client.engine.cio.CIO
|
||||||
|
import io.ktor.http.HttpMethod
|
||||||
|
import io.ktor.response.respondText
|
||||||
|
import io.ktor.routing.route
|
||||||
|
import io.ktor.routing.routing
|
||||||
|
|
||||||
|
fun Application.setupOauth() {
|
||||||
|
install(Authentication) {
|
||||||
|
oauth("oauth") {
|
||||||
|
urlProvider = { "http://localhost:8080/callback" }
|
||||||
|
client = HttpClient(CIO)
|
||||||
|
providerLookup = {
|
||||||
|
OAuthServerSettings.OAuth2ServerSettings(
|
||||||
|
name = "google",
|
||||||
|
authorizeUrl = "https://accounts.google.com/o/oauth2/auth",
|
||||||
|
accessTokenUrl = "https://accounts.google.com/o/oauth2/token",
|
||||||
|
requestMethod = HttpMethod.Post,
|
||||||
|
clientId = System.getenv("GOOGLE_CLIENT_ID"),
|
||||||
|
clientSecret = System.getenv("GOOGLE_CLIENT_SECRET"),
|
||||||
|
defaultScopes = listOf("https://www.googleapis.com/auth/userinfo.profile")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Application.configBasicAuth() {
|
||||||
|
install(Authentication) {
|
||||||
|
basic(AuthConfigName.Basic) {
|
||||||
|
realm = "Ktor Server"
|
||||||
|
validate { credentials ->
|
||||||
|
if (credentials.name == credentials.password) {
|
||||||
|
UserIdPrincipal(credentials.name)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Application.notarizedAuthRoute(authConfig: SecurityConfiguration) {
|
||||||
|
routing {
|
||||||
|
notarizedAuthenticate(authConfig) {
|
||||||
|
route("/test") { notarizedGet(testGetInfo(authConfig.name)) {
|
||||||
|
call.respondText { "hey dude ‼️ congratz on the get request" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Application.configJwtAuth() {
|
||||||
|
install(Authentication) {
|
||||||
|
jwt(AuthConfigName.JWT) {
|
||||||
|
realm = "Ktor server"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testGetInfo(vararg security: String) =
|
||||||
|
GetInfo<TestParams, TestResponse>(
|
||||||
|
summary = "Another get test",
|
||||||
|
description = "testing more",
|
||||||
|
responseInfo = TestResponseInfo.testGetResponse,
|
||||||
|
securitySchemes = security.toSet()
|
||||||
|
)
|
||||||
|
|
||||||
|
object AuthConfigName {
|
||||||
|
const val Basic = "basic"
|
||||||
|
const val JWT = "jwt"
|
||||||
|
const val OAuth = "oauth"
|
||||||
|
}
|
@ -1,75 +1,94 @@
|
|||||||
{
|
{
|
||||||
"openapi" : "3.0.3",
|
"openapi": "3.0.3",
|
||||||
"info" : { },
|
"info": {
|
||||||
"servers" : [ ],
|
"title": "Test API",
|
||||||
"paths" : {
|
"version": "1.33.7",
|
||||||
"/test" : {
|
"description": "An amazing, fully-ish 😉 generated API spec",
|
||||||
"get" : {
|
"termsOfService": "https://example.com",
|
||||||
"tags" : [ ],
|
"contact": {
|
||||||
"summary" : "Another get test",
|
"name": "Homer Simpson",
|
||||||
"description" : "testing more",
|
"url": "https://gph.is/1NPUDiM",
|
||||||
"parameters" : [ {
|
"email": "chunkylover53@aol.com"
|
||||||
"name" : "a",
|
},
|
||||||
"in" : "path",
|
"license": {
|
||||||
"schema" : {
|
"name": "MIT",
|
||||||
"type" : "string"
|
"url": "https://github.com/bkbnio/kompendium/blob/main/LICENSE"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"servers": [
|
||||||
|
{
|
||||||
|
"url": "https://myawesomeapi.com",
|
||||||
|
"description": "Production instance of my API"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://staging.myawesomeapi.com",
|
||||||
|
"description": "Where the fun stuff happens"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"/test": {
|
||||||
|
"get": {
|
||||||
|
"tags": [],
|
||||||
|
"summary": "Another get test",
|
||||||
|
"description": "testing more",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "a",
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"required": true,
|
||||||
|
"deprecated": false
|
||||||
},
|
},
|
||||||
"required" : true,
|
{
|
||||||
"deprecated" : false
|
"name": "aa",
|
||||||
}, {
|
"in": "query",
|
||||||
"name" : "aa",
|
"schema": {
|
||||||
"in" : "query",
|
"format": "int32",
|
||||||
"schema" : {
|
"type": "integer"
|
||||||
"format" : "int32",
|
},
|
||||||
"type" : "integer"
|
"required": true,
|
||||||
},
|
"deprecated": false
|
||||||
"required" : true,
|
}
|
||||||
"deprecated" : false
|
],
|
||||||
} ],
|
"responses": {
|
||||||
"responses" : {
|
"200": {
|
||||||
"200" : {
|
"description": "A Successful Endeavor",
|
||||||
"description" : "A Successful Endeavor",
|
"content": {
|
||||||
"content" : {
|
"application/json": {
|
||||||
"application/json" : {
|
"schema": {
|
||||||
"schema" : {
|
"properties": {
|
||||||
"$ref" : "#/components/schemas/TestResponse"
|
"c": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"c"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"deprecated" : false,
|
"deprecated": false,
|
||||||
"security" : [ {
|
"security": [
|
||||||
"basic" : [ ]
|
{
|
||||||
} ]
|
"basic": []
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"components" : {
|
|
||||||
"schemas" : {
|
|
||||||
"String" : {
|
|
||||||
"type" : "string"
|
|
||||||
},
|
|
||||||
"TestResponse" : {
|
|
||||||
"properties" : {
|
|
||||||
"c" : {
|
|
||||||
"$ref" : "#/components/schemas/String"
|
|
||||||
}
|
}
|
||||||
},
|
]
|
||||||
"type" : "object"
|
|
||||||
},
|
|
||||||
"Int" : {
|
|
||||||
"format" : "int32",
|
|
||||||
"type" : "integer"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"securitySchemes" : {
|
|
||||||
"basic" : {
|
|
||||||
"type" : "http",
|
|
||||||
"scheme" : "basic"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"security" : [ ],
|
"components": {
|
||||||
"tags" : [ ]
|
"securitySchemes": {
|
||||||
|
"basic": {
|
||||||
|
"type": "http",
|
||||||
|
"scheme": "basic"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [],
|
||||||
|
"tags": []
|
||||||
}
|
}
|
||||||
|
@ -1,75 +1,95 @@
|
|||||||
{
|
{
|
||||||
"openapi" : "3.0.3",
|
"openapi": "3.0.3",
|
||||||
"info" : { },
|
"info": {
|
||||||
"servers" : [ ],
|
"title": "Test API",
|
||||||
"paths" : {
|
"version": "1.33.7",
|
||||||
"/test" : {
|
"description": "An amazing, fully-ish 😉 generated API spec",
|
||||||
"get" : {
|
"termsOfService": "https://example.com",
|
||||||
"tags" : [ ],
|
"contact": {
|
||||||
"summary" : "Another get test",
|
"name": "Homer Simpson",
|
||||||
"description" : "testing more",
|
"url": "https://gph.is/1NPUDiM",
|
||||||
"parameters" : [ {
|
"email": "chunkylover53@aol.com"
|
||||||
"name" : "a",
|
},
|
||||||
"in" : "path",
|
"license": {
|
||||||
"schema" : {
|
"name": "MIT",
|
||||||
"type" : "string"
|
"url": "https://github.com/bkbnio/kompendium/blob/main/LICENSE"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"servers": [
|
||||||
|
{
|
||||||
|
"url": "https://myawesomeapi.com",
|
||||||
|
"description": "Production instance of my API"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://staging.myawesomeapi.com",
|
||||||
|
"description": "Where the fun stuff happens"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"/test": {
|
||||||
|
"get": {
|
||||||
|
"tags": [],
|
||||||
|
"summary": "Another get test",
|
||||||
|
"description": "testing more",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "a",
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"required": true,
|
||||||
|
"deprecated": false
|
||||||
},
|
},
|
||||||
"required" : true,
|
{
|
||||||
"deprecated" : false
|
"name": "aa",
|
||||||
}, {
|
"in": "query",
|
||||||
"name" : "aa",
|
"schema": {
|
||||||
"in" : "query",
|
"format": "int32",
|
||||||
"schema" : {
|
"type": "integer"
|
||||||
"format" : "int32",
|
},
|
||||||
"type" : "integer"
|
"required": true,
|
||||||
},
|
"deprecated": false
|
||||||
"required" : true,
|
}
|
||||||
"deprecated" : false
|
],
|
||||||
} ],
|
"responses": {
|
||||||
"responses" : {
|
"200": {
|
||||||
"200" : {
|
"description": "A Successful Endeavor",
|
||||||
"description" : "A Successful Endeavor",
|
"content": {
|
||||||
"content" : {
|
"application/json": {
|
||||||
"application/json" : {
|
"schema": {
|
||||||
"schema" : {
|
"properties": {
|
||||||
"$ref" : "#/components/schemas/TestResponse"
|
"c": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"c"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"deprecated" : false,
|
"deprecated": false,
|
||||||
"security" : [ {
|
"security": [
|
||||||
"jwt" : [ ]
|
{
|
||||||
} ]
|
"jwt": []
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"components" : {
|
|
||||||
"schemas" : {
|
|
||||||
"String" : {
|
|
||||||
"type" : "string"
|
|
||||||
},
|
|
||||||
"TestResponse" : {
|
|
||||||
"properties" : {
|
|
||||||
"c" : {
|
|
||||||
"$ref" : "#/components/schemas/String"
|
|
||||||
}
|
}
|
||||||
},
|
]
|
||||||
"type" : "object"
|
|
||||||
},
|
|
||||||
"Int" : {
|
|
||||||
"format" : "int32",
|
|
||||||
"type" : "integer"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"securitySchemes" : {
|
|
||||||
"jwt" : {
|
|
||||||
"type" : "http",
|
|
||||||
"scheme" : "bearer"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"security" : [ ],
|
"components": {
|
||||||
"tags" : [ ]
|
"securitySchemes": {
|
||||||
|
"jwt": {
|
||||||
|
"bearerFormat": "JWT",
|
||||||
|
"type": "http",
|
||||||
|
"scheme": "bearer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [],
|
||||||
|
"tags": []
|
||||||
}
|
}
|
||||||
|
@ -1,76 +0,0 @@
|
|||||||
{
|
|
||||||
"openapi" : "3.0.3",
|
|
||||||
"info" : { },
|
|
||||||
"servers" : [ ],
|
|
||||||
"paths" : {
|
|
||||||
"/test" : {
|
|
||||||
"get" : {
|
|
||||||
"tags" : [ ],
|
|
||||||
"summary" : "Another get test",
|
|
||||||
"description" : "testing more",
|
|
||||||
"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" : "A Successful Endeavor",
|
|
||||||
"content" : {
|
|
||||||
"application/json" : {
|
|
||||||
"schema" : {
|
|
||||||
"$ref" : "#/components/schemas/TestResponse"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"deprecated" : false,
|
|
||||||
"security" : [ {
|
|
||||||
"jwt" : [ ]
|
|
||||||
} ]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"components" : {
|
|
||||||
"schemas" : {
|
|
||||||
"String" : {
|
|
||||||
"type" : "string"
|
|
||||||
},
|
|
||||||
"TestResponse" : {
|
|
||||||
"properties" : {
|
|
||||||
"c" : {
|
|
||||||
"$ref" : "#/components/schemas/String"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"type" : "object"
|
|
||||||
},
|
|
||||||
"Int" : {
|
|
||||||
"format" : "int32",
|
|
||||||
"type" : "integer"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"securitySchemes" : {
|
|
||||||
"jwt" : {
|
|
||||||
"type" : "apiKey",
|
|
||||||
"name" : "x-api-key",
|
|
||||||
"in" : "header"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"security" : [ ],
|
|
||||||
"tags" : [ ]
|
|
||||||
}
|
|
@ -1,75 +0,0 @@
|
|||||||
{
|
|
||||||
"openapi" : "3.0.3",
|
|
||||||
"info" : { },
|
|
||||||
"servers" : [ ],
|
|
||||||
"paths" : {
|
|
||||||
"/test" : {
|
|
||||||
"get" : {
|
|
||||||
"tags" : [ ],
|
|
||||||
"summary" : "Another get test",
|
|
||||||
"description" : "testing more",
|
|
||||||
"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" : "A Successful Endeavor",
|
|
||||||
"content" : {
|
|
||||||
"application/json" : {
|
|
||||||
"schema" : {
|
|
||||||
"$ref" : "#/components/schemas/TestResponse"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"deprecated" : false,
|
|
||||||
"security" : [ {
|
|
||||||
"jwt" : [ ]
|
|
||||||
} ]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"components" : {
|
|
||||||
"schemas" : {
|
|
||||||
"String" : {
|
|
||||||
"type" : "string"
|
|
||||||
},
|
|
||||||
"TestResponse" : {
|
|
||||||
"properties" : {
|
|
||||||
"c" : {
|
|
||||||
"$ref" : "#/components/schemas/String"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"type" : "object"
|
|
||||||
},
|
|
||||||
"Int" : {
|
|
||||||
"format" : "int32",
|
|
||||||
"type" : "integer"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"securitySchemes" : {
|
|
||||||
"jwt" : {
|
|
||||||
"type" : "http",
|
|
||||||
"scheme" : "oauth"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"security" : [ ],
|
|
||||||
"tags" : [ ]
|
|
||||||
}
|
|
@ -1,82 +0,0 @@
|
|||||||
{
|
|
||||||
"openapi" : "3.0.3",
|
|
||||||
"info" : { },
|
|
||||||
"servers" : [ ],
|
|
||||||
"paths" : {
|
|
||||||
"/test" : {
|
|
||||||
"get" : {
|
|
||||||
"tags" : [ ],
|
|
||||||
"summary" : "Another get test",
|
|
||||||
"description" : "testing more",
|
|
||||||
"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" : "A Successful Endeavor",
|
|
||||||
"content" : {
|
|
||||||
"application/json" : {
|
|
||||||
"schema" : {
|
|
||||||
"$ref" : "#/components/schemas/TestResponse"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"deprecated" : false,
|
|
||||||
"security" : [ {
|
|
||||||
"jwt1" : [ ],
|
|
||||||
"jwt2" : [ ]
|
|
||||||
} ]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"components" : {
|
|
||||||
"schemas" : {
|
|
||||||
"String" : {
|
|
||||||
"type" : "string"
|
|
||||||
},
|
|
||||||
"TestResponse" : {
|
|
||||||
"properties" : {
|
|
||||||
"c" : {
|
|
||||||
"$ref" : "#/components/schemas/String"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"type" : "object"
|
|
||||||
},
|
|
||||||
"Int" : {
|
|
||||||
"format" : "int32",
|
|
||||||
"type" : "integer"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"securitySchemes" : {
|
|
||||||
"jwt1" : {
|
|
||||||
"type" : "apiKey",
|
|
||||||
"name" : "x-api-key-1",
|
|
||||||
"in" : "header"
|
|
||||||
},
|
|
||||||
"jwt2" : {
|
|
||||||
"type" : "apiKey",
|
|
||||||
"name" : "x-api-key-2",
|
|
||||||
"in" : "header"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"security" : [ ],
|
|
||||||
"tags" : [ ]
|
|
||||||
}
|
|
@ -0,0 +1,114 @@
|
|||||||
|
{
|
||||||
|
"openapi": "3.0.3",
|
||||||
|
"info": {
|
||||||
|
"title": "Test API",
|
||||||
|
"version": "1.33.7",
|
||||||
|
"description": "An amazing, fully-ish 😉 generated API spec",
|
||||||
|
"termsOfService": "https://example.com",
|
||||||
|
"contact": {
|
||||||
|
"name": "Homer Simpson",
|
||||||
|
"url": "https://gph.is/1NPUDiM",
|
||||||
|
"email": "chunkylover53@aol.com"
|
||||||
|
},
|
||||||
|
"license": {
|
||||||
|
"name": "MIT",
|
||||||
|
"url": "https://github.com/bkbnio/kompendium/blob/main/LICENSE"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"servers": [
|
||||||
|
{
|
||||||
|
"url": "https://myawesomeapi.com",
|
||||||
|
"description": "Production instance of my API"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://staging.myawesomeapi.com",
|
||||||
|
"description": "Where the fun stuff happens"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"/test": {
|
||||||
|
"get": {
|
||||||
|
"tags": [],
|
||||||
|
"summary": "Another get test",
|
||||||
|
"description": "testing more",
|
||||||
|
"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": "A Successful Endeavor",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"properties": {
|
||||||
|
"c": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"c"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"deprecated": false,
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"oauth": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"securitySchemes": {
|
||||||
|
"oauth": {
|
||||||
|
"flows": {
|
||||||
|
"implicit": {
|
||||||
|
"authorizationUrl": "https://accounts.google.com/o/oauth2/auth",
|
||||||
|
"scopes": {
|
||||||
|
"test": "is a cool scope",
|
||||||
|
"this": "is also cool"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"authorizationCode": {
|
||||||
|
"authorizationUrl": "https://accounts.google.com/o/oauth2/auth",
|
||||||
|
"scopes": {}
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"tokenUrl": "https://accounts.google.com/o/oauth2/auth",
|
||||||
|
"scopes": {}
|
||||||
|
},
|
||||||
|
"clientCredentials": {
|
||||||
|
"tokenUrl": "https://accounts.google.com/token",
|
||||||
|
"scopes": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "oauth2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [],
|
||||||
|
"tags": []
|
||||||
|
}
|
100
kompendium-core/Module.md
Normal file
100
kompendium-core/Module.md
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
# Module kompendium-core
|
||||||
|
|
||||||
|
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, 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,74 +1,61 @@
|
|||||||
plugins {
|
plugins {
|
||||||
`java-library`
|
kotlin("jvm")
|
||||||
`maven-publish`
|
id("io.bkbn.sourdough.library.jvm") version "0.5.4"
|
||||||
signing
|
id("io.gitlab.arturbosch.detekt") version "1.19.0"
|
||||||
|
id("com.adarshr.test-logger") version "3.1.0"
|
||||||
|
id("org.jetbrains.dokka")
|
||||||
|
id("maven-publish")
|
||||||
|
id("java-library")
|
||||||
|
id("signing")
|
||||||
|
id("java-test-fixtures")
|
||||||
|
}
|
||||||
|
|
||||||
|
sourdough {
|
||||||
|
githubOrg.set("bkbnio")
|
||||||
|
githubRepo.set("kompendium")
|
||||||
|
libraryName.set("Kompendium Core")
|
||||||
|
libraryDescription.set("Core functionality for the Kompendium library")
|
||||||
|
licenseName.set("MIT License")
|
||||||
|
licenseUrl.set("https://mit-license.org")
|
||||||
|
developerId.set("unredundant")
|
||||||
|
developerName.set("Ryan Brink")
|
||||||
|
developerEmail.set("admin@bkbn.io")
|
||||||
|
compilerArgs.set(listOf("-opt-in=kotlin.RequiresOptIn"))
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(platform("org.jetbrains.kotlin:kotlin-bom"))
|
// VERSIONS
|
||||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
|
val ktorVersion: String by project
|
||||||
implementation(libs.bundles.ktor)
|
val kotestVersion: String by project
|
||||||
testImplementation("org.jetbrains.kotlin:kotlin-test")
|
|
||||||
testImplementation("org.jetbrains.kotlin:kotlin-test-junit")
|
// IMPLEMENTATION
|
||||||
testImplementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.12.0")
|
|
||||||
testImplementation("io.ktor:ktor-server-test-host:1.5.3")
|
api(projects.kompendiumOas)
|
||||||
|
api(projects.kompendiumAnnotations)
|
||||||
|
|
||||||
|
implementation(group = "io.ktor", name = "ktor-server-core", version = ktorVersion)
|
||||||
|
implementation(group = "io.ktor", name = "ktor-html-builder", version = ktorVersion)
|
||||||
|
|
||||||
|
// TEST FIXTURES
|
||||||
|
|
||||||
|
testFixturesApi(group = "io.kotest", name = "kotest-runner-junit5-jvm", version = kotestVersion)
|
||||||
|
testFixturesApi(group = "io.kotest", name = "kotest-assertions-core-jvm", version = kotestVersion)
|
||||||
|
testFixturesApi(group = "io.kotest", name = "kotest-property-jvm", version = kotestVersion)
|
||||||
|
testFixturesApi(group = "io.kotest", name = "kotest-assertions-json-jvm", version = kotestVersion)
|
||||||
|
testFixturesApi(group = "io.kotest", name = "kotest-assertions-ktor-jvm", version = "4.4.3")
|
||||||
|
|
||||||
|
testFixturesApi(group = "io.ktor", name = "ktor-server-core", version = ktorVersion)
|
||||||
|
testFixturesApi(group = "io.ktor", name = "ktor-server-test-host", version = ktorVersion)
|
||||||
|
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.2")
|
||||||
}
|
}
|
||||||
|
|
||||||
java {
|
testing {
|
||||||
withSourcesJar()
|
suites {
|
||||||
withJavadocJar()
|
named("test", JvmTestSuite::class) {
|
||||||
}
|
useJUnitJupiter()
|
||||||
|
|
||||||
publishing {
|
|
||||||
repositories {
|
|
||||||
maven {
|
|
||||||
name = "GithubPackages"
|
|
||||||
url = uri("https://maven.pkg.github.com/bkbnio/kompendium")
|
|
||||||
credentials {
|
|
||||||
username = System.getenv("GITHUB_ACTOR")
|
|
||||||
password = System.getenv("GITHUB_TOKEN")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
publications {
|
|
||||||
create<MavenPublication>("kompendium") {
|
|
||||||
from(components["kotlin"])
|
|
||||||
artifact(tasks.sourcesJar)
|
|
||||||
artifact(tasks.javadocJar)
|
|
||||||
groupId = project.group.toString()
|
|
||||||
artifactId = project.name.toLowerCase()
|
|
||||||
version = project.version.toString()
|
|
||||||
|
|
||||||
pom {
|
|
||||||
name.set("Kompendium")
|
|
||||||
description.set("A minimally invasive OpenAPI spec generator for Ktor")
|
|
||||||
url.set("https://github.com/bkbnio/Kompendium")
|
|
||||||
licenses {
|
|
||||||
license {
|
|
||||||
name.set("MIT License")
|
|
||||||
url.set("https://mit-license.org/")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
developers {
|
|
||||||
developer {
|
|
||||||
id.set("bkbnio")
|
|
||||||
name.set("Ryan Brink")
|
|
||||||
email.set("admin@bkbn.io")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
scm {
|
|
||||||
connection.set("scm:git:git://github.com/bkbnio/Kompendium.git")
|
|
||||||
developerConnection.set("scm:git:ssh://github.com/bkbnio/Kompendium.git")
|
|
||||||
url.set("https://github.com/bkbnio/Kompendium.git")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
signing {
|
|
||||||
val signingKey: String? by project
|
|
||||||
val signingPassword: String? by project
|
|
||||||
useInMemoryPgpKeys(signingKey, signingPassword)
|
|
||||||
sign(publishing.publications)
|
|
||||||
}
|
|
||||||
|
@ -1,34 +0,0 @@
|
|||||||
package io.bkbn.kompendium
|
|
||||||
|
|
||||||
import io.bkbn.kompendium.models.meta.ErrorMap
|
|
||||||
import io.bkbn.kompendium.models.meta.SchemaMap
|
|
||||||
import io.bkbn.kompendium.models.oas.OpenApiSpec
|
|
||||||
import io.bkbn.kompendium.models.oas.OpenApiSpecInfo
|
|
||||||
import io.bkbn.kompendium.path.CorePathCalculator
|
|
||||||
import io.bkbn.kompendium.path.PathCalculator
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Maintains all state for the Kompendium library
|
|
||||||
*/
|
|
||||||
object Kompendium {
|
|
||||||
|
|
||||||
var errorMap: ErrorMap = emptyMap()
|
|
||||||
var cache: SchemaMap = emptyMap()
|
|
||||||
|
|
||||||
var openApiSpec = OpenApiSpec(
|
|
||||||
info = OpenApiSpecInfo(),
|
|
||||||
servers = mutableListOf(),
|
|
||||||
paths = mutableMapOf()
|
|
||||||
)
|
|
||||||
|
|
||||||
var pathCalculator: PathCalculator = CorePathCalculator()
|
|
||||||
|
|
||||||
fun resetSchema() {
|
|
||||||
openApiSpec = OpenApiSpec(
|
|
||||||
info = OpenApiSpecInfo(),
|
|
||||||
servers = mutableListOf(),
|
|
||||||
paths = mutableMapOf()
|
|
||||||
)
|
|
||||||
cache = emptyMap()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,71 +0,0 @@
|
|||||||
package io.bkbn.kompendium
|
|
||||||
|
|
||||||
import io.ktor.routing.Route
|
|
||||||
import kotlin.reflect.KClass
|
|
||||||
import kotlin.reflect.KType
|
|
||||||
import kotlin.reflect.full.createType
|
|
||||||
import kotlin.reflect.typeOf
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Functions are considered preflight when they are used to intercept a method ahead of running.
|
|
||||||
*/
|
|
||||||
object KompendiumPreFlight {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Performs all content analysis on the types provided to a notarized route and adds it to the top level spec
|
|
||||||
* @param TParam
|
|
||||||
* @param TReq
|
|
||||||
* @param TResp
|
|
||||||
* @param block The function to execute, provided type information of the parameters above
|
|
||||||
* @return [Route]
|
|
||||||
*/
|
|
||||||
@OptIn(ExperimentalStdlibApi::class)
|
|
||||||
inline fun <reified TParam : Any, reified TReq : Any, reified TResp : Any> methodNotarizationPreFlight(
|
|
||||||
block: (KType, KType, KType) -> Route
|
|
||||||
): Route {
|
|
||||||
val requestType = typeOf<TReq>()
|
|
||||||
val responseType = typeOf<TResp>()
|
|
||||||
val paramType = typeOf<TParam>()
|
|
||||||
addToCache(paramType, requestType, responseType)
|
|
||||||
Kompendium.openApiSpec.components.schemas.putAll(Kompendium.cache)
|
|
||||||
return block.invoke(paramType, requestType, responseType)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Performs all content analysis on the types provided to a notarized error and adds them to the top level spec.
|
|
||||||
* @param TErr
|
|
||||||
* @param TResp
|
|
||||||
* @param block The function to execute, provided type information of the parameters above
|
|
||||||
*/
|
|
||||||
@OptIn(ExperimentalStdlibApi::class)
|
|
||||||
inline fun <reified TErr : Throwable, reified TResp : Any> errorNotarizationPreFlight(
|
|
||||||
block: (KType, KType) -> Unit
|
|
||||||
) {
|
|
||||||
val errorType = typeOf<TErr>()
|
|
||||||
val responseType = typeOf<TResp>()
|
|
||||||
addToCache(typeOf<Unit>(), typeOf<Unit>(), responseType)
|
|
||||||
Kompendium.openApiSpec.components.schemas.putAll(Kompendium.cache)
|
|
||||||
return block.invoke(errorType, responseType)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addToCache(paramType: KType, requestType: KType, responseType: KType) {
|
|
||||||
gatherSubTypes(requestType).forEach {
|
|
||||||
Kompendium.cache = Kontent.generateKontent(it, Kompendium.cache)
|
|
||||||
}
|
|
||||||
gatherSubTypes(responseType).forEach {
|
|
||||||
Kompendium.cache = Kontent.generateKontent(it, Kompendium.cache)
|
|
||||||
}
|
|
||||||
Kompendium.cache = Kontent.generateParameterKontent(paramType, Kompendium.cache)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun gatherSubTypes(type: KType): List<KType> {
|
|
||||||
val classifier = type.classifier as KClass<*>
|
|
||||||
return if (classifier.isSealed) {
|
|
||||||
classifier.sealedSubclasses.map {
|
|
||||||
it.createType(type.arguments)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
listOf(type)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,244 +0,0 @@
|
|||||||
package io.bkbn.kompendium
|
|
||||||
|
|
||||||
import io.bkbn.kompendium.models.meta.SchemaMap
|
|
||||||
import io.bkbn.kompendium.models.oas.AnyOfReferencedSchema
|
|
||||||
import io.bkbn.kompendium.models.oas.ArraySchema
|
|
||||||
import io.bkbn.kompendium.models.oas.DictionarySchema
|
|
||||||
import io.bkbn.kompendium.models.oas.EnumSchema
|
|
||||||
import io.bkbn.kompendium.models.oas.FormatSchema
|
|
||||||
import io.bkbn.kompendium.models.oas.ObjectSchema
|
|
||||||
import io.bkbn.kompendium.models.oas.ReferencedSchema
|
|
||||||
import io.bkbn.kompendium.models.oas.SimpleSchema
|
|
||||||
import io.bkbn.kompendium.util.Helpers.COMPONENT_SLUG
|
|
||||||
import io.bkbn.kompendium.util.Helpers.genericNameAdapter
|
|
||||||
import io.bkbn.kompendium.util.Helpers.getReferenceSlug
|
|
||||||
import io.bkbn.kompendium.util.Helpers.getSimpleSlug
|
|
||||||
import io.bkbn.kompendium.util.Helpers.logged
|
|
||||||
import java.util.UUID
|
|
||||||
import kotlin.reflect.KClass
|
|
||||||
import kotlin.reflect.KType
|
|
||||||
import kotlin.reflect.full.createType
|
|
||||||
import kotlin.reflect.full.isSubclassOf
|
|
||||||
import kotlin.reflect.full.memberProperties
|
|
||||||
import kotlin.reflect.jvm.javaField
|
|
||||||
import kotlin.reflect.typeOf
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Responsible for generating the schema map that is used to power all object references across the API Spec.
|
|
||||||
*/
|
|
||||||
object Kontent {
|
|
||||||
|
|
||||||
private val logger = LoggerFactory.getLogger(javaClass)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Analyzes a type [T] for its top-level and any nested schemas, and adds them to a [SchemaMap], if provided
|
|
||||||
* @param T type to analyze
|
|
||||||
* @param cache Existing schema map to append to
|
|
||||||
* @return an updated schema map containing all type information for [T]
|
|
||||||
*/
|
|
||||||
@OptIn(ExperimentalStdlibApi::class)
|
|
||||||
inline fun <reified T> generateKontent(
|
|
||||||
cache: SchemaMap = emptyMap()
|
|
||||||
): SchemaMap {
|
|
||||||
val kontentType = typeOf<T>()
|
|
||||||
return generateKTypeKontent(kontentType, cache)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Analyzes a [KType] for its top-level and any nested schemas, and adds them to a [SchemaMap], if provided
|
|
||||||
* @param type [KType] to analyze
|
|
||||||
* @param cache Existing schema map to append to
|
|
||||||
* @return an updated schema map containing all type information for [KType] type
|
|
||||||
*/
|
|
||||||
fun generateKontent(
|
|
||||||
type: KType,
|
|
||||||
cache: SchemaMap = emptyMap()
|
|
||||||
): SchemaMap {
|
|
||||||
return generateKTypeKontent(type, cache)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Analyze a type [T], but filters out the top-level type
|
|
||||||
* @param T type to analyze
|
|
||||||
* @param cache Existing schema map to append to
|
|
||||||
* @return an updated schema map containing all type information for [T]
|
|
||||||
*/
|
|
||||||
@OptIn(ExperimentalStdlibApi::class)
|
|
||||||
inline fun <reified T> generateParameterKontent(
|
|
||||||
cache: SchemaMap = emptyMap()
|
|
||||||
): SchemaMap {
|
|
||||||
val kontentType = typeOf<T>()
|
|
||||||
return generateKTypeKontent(kontentType, cache)
|
|
||||||
.filterNot { (slug, _) -> slug == (kontentType.classifier as KClass<*>).simpleName }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Analyze a type but filters out the top-level type
|
|
||||||
* @param type to analyze
|
|
||||||
* @param cache Existing schema map to append to
|
|
||||||
* @return an updated schema map containing all type information for [T]
|
|
||||||
*/
|
|
||||||
fun generateParameterKontent(
|
|
||||||
type: KType,
|
|
||||||
cache: SchemaMap = emptyMap()
|
|
||||||
): SchemaMap {
|
|
||||||
return generateKTypeKontent(type, cache)
|
|
||||||
.filterNot { (slug, _) -> slug == (type.classifier as KClass<*>).simpleName }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Recursively fills schema map depending on [KType] classifier
|
|
||||||
* @param type [KType] to parse
|
|
||||||
* @param cache Existing schema map to append to
|
|
||||||
*/
|
|
||||||
fun generateKTypeKontent(
|
|
||||||
type: KType,
|
|
||||||
cache: SchemaMap = emptyMap()
|
|
||||||
): SchemaMap = logged(object {}.javaClass.enclosingMethod.name, mapOf("cache" to cache)) {
|
|
||||||
logger.debug("Parsing Kontent of $type")
|
|
||||||
when (val clazz = type.classifier as KClass<*>) {
|
|
||||||
Unit::class -> cache
|
|
||||||
Int::class -> cache.plus(clazz.simpleName!! to FormatSchema("int32", "integer"))
|
|
||||||
Long::class -> cache.plus(clazz.simpleName!! to FormatSchema("int64", "integer"))
|
|
||||||
Double::class -> cache.plus(clazz.simpleName!! to FormatSchema("double", "number"))
|
|
||||||
Float::class -> cache.plus(clazz.simpleName!! to FormatSchema("float", "number"))
|
|
||||||
String::class -> cache.plus(clazz.simpleName!! to SimpleSchema("string"))
|
|
||||||
Boolean::class -> cache.plus(clazz.simpleName!! to SimpleSchema("boolean"))
|
|
||||||
UUID::class -> cache.plus(clazz.simpleName!! to FormatSchema("uuid", "string"))
|
|
||||||
else -> when {
|
|
||||||
clazz.isSubclassOf(Collection::class) -> handleCollectionType(type, clazz, cache)
|
|
||||||
clazz.isSubclassOf(Enum::class) -> handleEnumType(clazz, cache)
|
|
||||||
clazz.isSubclassOf(Map::class) -> handleMapType(type, clazz, cache)
|
|
||||||
else -> handleComplexType(type, clazz, cache)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* In the event of an object type, this method will parse out individual fields to recursively aggregate object map.
|
|
||||||
* @param clazz Class of the object to analyze
|
|
||||||
* @param cache Existing schema map to append to
|
|
||||||
*/
|
|
||||||
private fun handleComplexType(type: KType, clazz: KClass<*>, cache: SchemaMap): SchemaMap {
|
|
||||||
// This needs to be simple because it will be stored under it's appropriate reference component implicitly
|
|
||||||
val slug = type.getSimpleSlug()
|
|
||||||
// Only analyze if component has not already been stored in the cache
|
|
||||||
return when (cache.containsKey(slug)) {
|
|
||||||
true -> {
|
|
||||||
logger.debug("Cache already contains $slug, returning cache untouched")
|
|
||||||
cache
|
|
||||||
}
|
|
||||||
false -> {
|
|
||||||
logger.debug("$slug was not found in cache, generating now")
|
|
||||||
var newCache = cache
|
|
||||||
// Grabs any type parameters as a zip with the corresponding type argument
|
|
||||||
val typeMap = clazz.typeParameters.zip(type.arguments).toMap()
|
|
||||||
// associates each member with a Pair of prop name to property schema
|
|
||||||
val fieldMap = clazz.memberProperties.associate { prop ->
|
|
||||||
logger.debug("Analyzing $prop in class $clazz")
|
|
||||||
// Grab the field of the current property
|
|
||||||
val field = prop.javaField?.type?.kotlin ?: error("Unable to parse field type from $prop")
|
|
||||||
logger.debug("Detected field $field")
|
|
||||||
// Yoinks any generic types from the type map should the field be a generic
|
|
||||||
val yoinkBaseType = if (typeMap.containsKey(prop.returnType.classifier)) {
|
|
||||||
logger.debug("Generic type detected")
|
|
||||||
typeMap[prop.returnType.classifier]?.type!!
|
|
||||||
} else {
|
|
||||||
prop.returnType
|
|
||||||
}
|
|
||||||
// converts the base type to a class
|
|
||||||
val yoinkedClassifier = yoinkBaseType.classifier as KClass<*>
|
|
||||||
// in the event of a sealed class, grab all sealed subclasses and create a type from the base args
|
|
||||||
val yoinkedTypes = if (yoinkedClassifier.isSealed) {
|
|
||||||
yoinkedClassifier.sealedSubclasses.map { it.createType(yoinkBaseType.arguments) }
|
|
||||||
} else {
|
|
||||||
listOf(yoinkBaseType)
|
|
||||||
}
|
|
||||||
// if the most up-to-date cache does not contain the content for this field, generate it and add to cache
|
|
||||||
if (!newCache.containsKey(field.simpleName)) {
|
|
||||||
logger.debug("Cache was missing ${field.simpleName}, adding now")
|
|
||||||
yoinkedTypes.forEach {
|
|
||||||
newCache = generateKTypeKontent(it, newCache)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// TODO This in particular is worthy of a refactor... just not very well written
|
|
||||||
// builds the appropriate property schema based on the property return type
|
|
||||||
val propSchema = if (typeMap.containsKey(prop.returnType.classifier)) {
|
|
||||||
if (yoinkedClassifier.isSealed) {
|
|
||||||
val refs = yoinkedClassifier.sealedSubclasses
|
|
||||||
.map { it.createType(yoinkBaseType.arguments) }
|
|
||||||
.map { ReferencedSchema(it.getReferenceSlug()) }
|
|
||||||
AnyOfReferencedSchema(refs)
|
|
||||||
} else {
|
|
||||||
ReferencedSchema(typeMap[prop.returnType.classifier]?.type!!.getReferenceSlug())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (yoinkedClassifier.isSealed) {
|
|
||||||
val refs = yoinkedClassifier.sealedSubclasses
|
|
||||||
.map { it.createType(yoinkBaseType.arguments) }
|
|
||||||
.map { ReferencedSchema(it.getReferenceSlug()) }
|
|
||||||
AnyOfReferencedSchema(refs)
|
|
||||||
} else {
|
|
||||||
ReferencedSchema(field.getReferenceSlug(prop))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Pair(prop.name, propSchema)
|
|
||||||
}
|
|
||||||
logger.debug("$slug contains $fieldMap")
|
|
||||||
val schema = ObjectSchema(fieldMap)
|
|
||||||
logger.debug("$slug schema: $schema")
|
|
||||||
newCache.plus(slug to schema)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handler for when an [Enum] is encountered
|
|
||||||
* @param clazz Class of the object to analyze
|
|
||||||
* @param cache Existing schema map to append to
|
|
||||||
*/
|
|
||||||
private fun handleEnumType(clazz: KClass<*>, cache: SchemaMap): SchemaMap {
|
|
||||||
val options = clazz.java.enumConstants.map { it.toString() }.toSet()
|
|
||||||
return cache.plus(clazz.simpleName!! to EnumSchema(options))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handler for when a [Map] is encountered
|
|
||||||
* @param type Map type information
|
|
||||||
* @param clazz Map class information
|
|
||||||
* @param cache Existing schema map to append to
|
|
||||||
*/
|
|
||||||
private fun handleMapType(type: KType, clazz: KClass<*>, cache: SchemaMap): SchemaMap {
|
|
||||||
logger.debug("Map detected for $type, generating schema and appending to cache")
|
|
||||||
val (keyType, valType) = type.arguments.map { it.type }
|
|
||||||
logger.debug("Obtained map types -> key: $keyType and value: $valType")
|
|
||||||
if (keyType?.classifier != String::class) {
|
|
||||||
error("Invalid Map $type: OpenAPI dictionaries must have keys of type String")
|
|
||||||
}
|
|
||||||
val valClassName = (valType?.classifier as KClass<*>).simpleName
|
|
||||||
val referenceName = genericNameAdapter(type, clazz)
|
|
||||||
val valueReference = ReferencedSchema("$COMPONENT_SLUG/$valClassName")
|
|
||||||
val schema = DictionarySchema(additionalProperties = valueReference)
|
|
||||||
val updatedCache = generateKTypeKontent(valType, cache)
|
|
||||||
return updatedCache.plus(referenceName to schema)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handler for when a [Collection] is encountered
|
|
||||||
* @param type Collection type information
|
|
||||||
* @param clazz Collection class information
|
|
||||||
* @param cache Existing schema map to append to
|
|
||||||
*/
|
|
||||||
private fun handleCollectionType(type: KType, clazz: KClass<*>, cache: SchemaMap): SchemaMap {
|
|
||||||
logger.debug("Collection detected for $type, generating schema and appending to cache")
|
|
||||||
val collectionType = type.arguments.first().type!!
|
|
||||||
val collectionClass = collectionType.classifier as KClass<*>
|
|
||||||
logger.debug("Obtained collection class: $collectionClass")
|
|
||||||
val referenceName = genericNameAdapter(type, clazz)
|
|
||||||
val valueReference = ReferencedSchema("${COMPONENT_SLUG}/${collectionClass.simpleName}")
|
|
||||||
val schema = ArraySchema(items = valueReference)
|
|
||||||
val updatedCache = generateKTypeKontent(collectionType, cache)
|
|
||||||
return updatedCache.plus(referenceName to schema)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,237 +0,0 @@
|
|||||||
package io.bkbn.kompendium
|
|
||||||
|
|
||||||
import io.bkbn.kompendium.annotations.KompendiumParam
|
|
||||||
import io.bkbn.kompendium.models.meta.MethodInfo
|
|
||||||
import io.bkbn.kompendium.models.meta.RequestInfo
|
|
||||||
import io.bkbn.kompendium.models.meta.ResponseInfo
|
|
||||||
import io.bkbn.kompendium.models.oas.ExampleWrapper
|
|
||||||
import io.bkbn.kompendium.models.oas.OpenApiAnyOf
|
|
||||||
import io.bkbn.kompendium.models.oas.OpenApiSpecMediaType
|
|
||||||
import io.bkbn.kompendium.models.oas.OpenApiSpecParameter
|
|
||||||
import io.bkbn.kompendium.models.oas.OpenApiSpecPathItemOperation
|
|
||||||
import io.bkbn.kompendium.models.oas.OpenApiSpecReferencable
|
|
||||||
import io.bkbn.kompendium.models.oas.OpenApiSpecReferenceObject
|
|
||||||
import io.bkbn.kompendium.models.oas.OpenApiSpecRequest
|
|
||||||
import io.bkbn.kompendium.models.oas.OpenApiSpecResponse
|
|
||||||
import io.bkbn.kompendium.util.Helpers
|
|
||||||
import io.bkbn.kompendium.util.Helpers.getReferenceSlug
|
|
||||||
import io.bkbn.kompendium.util.Helpers.getSimpleSlug
|
|
||||||
import java.util.Locale
|
|
||||||
import java.util.UUID
|
|
||||||
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.memberProperties
|
|
||||||
import kotlin.reflect.full.primaryConstructor
|
|
||||||
import kotlin.reflect.jvm.javaField
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The MethodParser is responsible for converting route metadata and types into an OpenAPI compatible data class.
|
|
||||||
*/
|
|
||||||
object MethodParser {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates the OpenAPI Path spec from provided metadata
|
|
||||||
* @param info implementation of the [MethodInfo] sealed class
|
|
||||||
* @param paramType Type of `TParam`
|
|
||||||
* @param requestType Type of `TReq` if required
|
|
||||||
* @param responseType Type of `TResp`
|
|
||||||
* @return object representing the OpenAPI Path spec.
|
|
||||||
*/
|
|
||||||
fun parseMethodInfo(
|
|
||||||
info: MethodInfo<*, *>,
|
|
||||||
paramType: KType,
|
|
||||||
requestType: KType,
|
|
||||||
responseType: KType
|
|
||||||
) = OpenApiSpecPathItemOperation(
|
|
||||||
summary = info.summary,
|
|
||||||
description = info.description,
|
|
||||||
tags = info.tags,
|
|
||||||
deprecated = info.deprecated,
|
|
||||||
parameters = paramType.toParameterSpec(),
|
|
||||||
responses = responseType.toResponseSpec(info.responseInfo)?.let { mapOf(it) }.let {
|
|
||||||
when (it) {
|
|
||||||
null -> {
|
|
||||||
val throwables = parseThrowables(info.canThrow)
|
|
||||||
when (throwables.isEmpty()) {
|
|
||||||
true -> null
|
|
||||||
false -> throwables
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> it.plus(parseThrowables(info.canThrow))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
requestBody = when (info) {
|
|
||||||
is MethodInfo.PutInfo<*, *, *> -> requestType.toRequestSpec(info.requestInfo)
|
|
||||||
is MethodInfo.PostInfo<*, *, *> -> requestType.toRequestSpec(info.requestInfo)
|
|
||||||
else -> null
|
|
||||||
},
|
|
||||||
security = if (info.securitySchemes.isNotEmpty()) listOf(
|
|
||||||
// TODO support scopes
|
|
||||||
info.securitySchemes.associateWith { listOf() }
|
|
||||||
) else null
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds the error to the [Kompendium.errorMap] for reference in notarized routes.
|
|
||||||
* @param errorType [KType] of the throwable being handled
|
|
||||||
* @param responseType [KType] the type of the response sent in event of error
|
|
||||||
*/
|
|
||||||
fun ResponseInfo<*>.parseErrorInfo(
|
|
||||||
errorType: KType,
|
|
||||||
responseType: KType
|
|
||||||
) {
|
|
||||||
Kompendium.errorMap = Kompendium.errorMap.plus(errorType to responseType.toResponseSpec(this))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses possible errors thrown by a route
|
|
||||||
* @param throwables Set of classes that can be thrown
|
|
||||||
* @return Mapping of status codes to their corresponding error spec
|
|
||||||
*/
|
|
||||||
private fun parseThrowables(throwables: Set<KClass<*>>): Map<Int, OpenApiSpecReferencable> = throwables.mapNotNull {
|
|
||||||
Kompendium.errorMap[it.createType()]
|
|
||||||
}.toMap()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a [KType] to an [OpenApiSpecRequest]
|
|
||||||
* @receiver [KType] to convert
|
|
||||||
* @param requestInfo request metadata
|
|
||||||
* @return Will return a generated [OpenApiSpecRequest] if requestInfo is not null
|
|
||||||
*/
|
|
||||||
private fun KType.toRequestSpec(requestInfo: RequestInfo<*>?): OpenApiSpecRequest<*>? =
|
|
||||||
when (requestInfo) {
|
|
||||||
null -> null
|
|
||||||
else -> {
|
|
||||||
OpenApiSpecRequest(
|
|
||||||
description = requestInfo.description,
|
|
||||||
content = resolveContent(this, requestInfo.mediaTypes, requestInfo.examples) ?: mapOf()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a [KType] to a pairing of http status code to [OpenApiSpecRequest]
|
|
||||||
* @receiver [KType] to convert
|
|
||||||
* @param responseInfo response metadata
|
|
||||||
* @return Will return a generated [Pair] if responseInfo is not null
|
|
||||||
*/
|
|
||||||
private fun KType.toResponseSpec(responseInfo: ResponseInfo<*>?): Pair<Int, OpenApiSpecResponse<*>>? =
|
|
||||||
when (responseInfo) {
|
|
||||||
null -> null
|
|
||||||
else -> {
|
|
||||||
val specResponse = OpenApiSpecResponse(
|
|
||||||
description = responseInfo.description,
|
|
||||||
content = resolveContent(this, responseInfo.mediaTypes, responseInfo.examples)
|
|
||||||
)
|
|
||||||
Pair(responseInfo.status.value, specResponse)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates MediaTypes along with any examples provided
|
|
||||||
* @param type [KType] Type of the object
|
|
||||||
* @param mediaTypes list of acceptable http media types
|
|
||||||
* @param examples Mapping of named examples of valid bodies.
|
|
||||||
* @return Named mapping of media types.
|
|
||||||
*/
|
|
||||||
private fun <F> resolveContent(
|
|
||||||
type: KType,
|
|
||||||
mediaTypes: List<String>,
|
|
||||||
examples: Map<String, F>
|
|
||||||
): Map<String, OpenApiSpecMediaType<F>>? {
|
|
||||||
val classifier = type.classifier as KClass<*>
|
|
||||||
return if (type != Helpers.UNIT_TYPE && mediaTypes.isNotEmpty()) {
|
|
||||||
mediaTypes.associateWith {
|
|
||||||
val schema = if (classifier.isSealed) {
|
|
||||||
val refs = classifier.sealedSubclasses
|
|
||||||
.map { it.createType(type.arguments) }
|
|
||||||
.map { it.getReferenceSlug() }
|
|
||||||
.map { OpenApiSpecReferenceObject(it) }
|
|
||||||
OpenApiAnyOf(refs)
|
|
||||||
} else {
|
|
||||||
val ref = type.getReferenceSlug()
|
|
||||||
OpenApiSpecReferenceObject(ref)
|
|
||||||
}
|
|
||||||
OpenApiSpecMediaType(
|
|
||||||
schema = schema,
|
|
||||||
examples = examples.mapValues { (_, v) -> ExampleWrapper(v) }.ifEmpty { null }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses a type for all parameter information. All fields in the receiver
|
|
||||||
* must be annotated with [io.bkbn.kompendium.annotations.KompendiumParam].
|
|
||||||
* @receiver type
|
|
||||||
* @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(): List<OpenApiSpecParameter> {
|
|
||||||
val clazz = classifier as KClass<*>
|
|
||||||
return clazz.memberProperties.map { prop ->
|
|
||||||
val field = prop.javaField?.type?.kotlin
|
|
||||||
?: error("Unable to parse field type from $prop")
|
|
||||||
val anny = prop.findAnnotation<KompendiumParam>()
|
|
||||||
?: error("Field ${prop.name} is not annotated with KompendiumParam")
|
|
||||||
val schema = Kompendium.cache[field.getSimpleSlug(prop)]
|
|
||||||
?: error("Could not find component type for $prop")
|
|
||||||
val defaultValue = getDefaultParameterValue(clazz, prop)
|
|
||||||
OpenApiSpecParameter(
|
|
||||||
name = prop.name,
|
|
||||||
`in` = anny.type.name.lowercase(Locale.getDefault()),
|
|
||||||
schema = schema.addDefault(defaultValue),
|
|
||||||
description = anny.description.ifBlank { null },
|
|
||||||
required = !prop.returnType.isMarkedNullable
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Absolutely disgusting reflection to determine if a default value is available for a given property.
|
|
||||||
* @param clazz to which the property belongs
|
|
||||||
* @param prop the property in question
|
|
||||||
* @return The default value if found
|
|
||||||
*/
|
|
||||||
private fun getDefaultParameterValue(clazz: KClass<*>, prop: KProperty<*>): Any? {
|
|
||||||
val constructor = clazz.primaryConstructor
|
|
||||||
val parameterInQuestion = constructor
|
|
||||||
?.parameters
|
|
||||||
?.find { it.name == prop.name }
|
|
||||||
?: error("could not find parameter ${prop.name}")
|
|
||||||
if (!parameterInQuestion.isOptional) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
val values = constructor
|
|
||||||
.parameters
|
|
||||||
.filterNot { it.isOptional }
|
|
||||||
.associateWith { defaultValueInjector(it) }
|
|
||||||
val instance = constructor.callBy(values)
|
|
||||||
val methods = clazz.java.methods
|
|
||||||
val getterName = "get${prop.name.capitalize()}"
|
|
||||||
val getterFunction = methods.find { it.name == getterName }
|
|
||||||
?: error("Could not associate ${prop.name} with a getter")
|
|
||||||
return getterFunction.invoke(instance)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Allows the reflection invoker to populate a parameter map with values in order to sus out any default parameters.
|
|
||||||
* @param param Parameter to provide value for
|
|
||||||
* @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) {
|
|
||||||
String::class -> "test"
|
|
||||||
Boolean::class -> false
|
|
||||||
Int::class -> 1
|
|
||||||
Long::class -> 2
|
|
||||||
Double::class -> 1.0
|
|
||||||
Float::class -> 1.0
|
|
||||||
UUID::class -> UUID.randomUUID()
|
|
||||||
else -> error("Unsupported Type")
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,116 +0,0 @@
|
|||||||
package io.bkbn.kompendium
|
|
||||||
|
|
||||||
import io.ktor.application.ApplicationCall
|
|
||||||
import io.ktor.features.StatusPages
|
|
||||||
import io.ktor.http.HttpMethod
|
|
||||||
import io.ktor.routing.Route
|
|
||||||
import io.ktor.routing.method
|
|
||||||
import io.ktor.util.pipeline.PipelineContext
|
|
||||||
import io.ktor.util.pipeline.PipelineInterceptor
|
|
||||||
import io.bkbn.kompendium.KompendiumPreFlight.errorNotarizationPreFlight
|
|
||||||
import io.bkbn.kompendium.MethodParser.parseErrorInfo
|
|
||||||
import io.bkbn.kompendium.MethodParser.parseMethodInfo
|
|
||||||
import io.bkbn.kompendium.models.meta.MethodInfo.GetInfo
|
|
||||||
import io.bkbn.kompendium.models.meta.MethodInfo.PostInfo
|
|
||||||
import io.bkbn.kompendium.models.meta.MethodInfo.PutInfo
|
|
||||||
import io.bkbn.kompendium.models.meta.MethodInfo.DeleteInfo
|
|
||||||
import io.bkbn.kompendium.models.meta.ResponseInfo
|
|
||||||
import io.bkbn.kompendium.models.oas.OpenApiSpecPathItem
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Notarization methods are the primary way that a Ktor API using Kompendium differentiates
|
|
||||||
* from a default Ktor application. On instantiation, a notarized route, provided with the proper metadata,
|
|
||||||
* will reflectively analyze all pertinent data to build a corresponding OpenAPI entry.
|
|
||||||
*/
|
|
||||||
object Notarized {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Notarization for an HTTP GET request
|
|
||||||
* @param TParam The class containing all parameter fields.
|
|
||||||
* Each field must be annotated with @[io.bkbn.kompendium.annotations.KompendiumField]
|
|
||||||
* @param TResp Class detailing the expected API response
|
|
||||||
* @param info Route metadata
|
|
||||||
*/
|
|
||||||
inline fun <reified TParam : Any, reified TResp : Any> Route.notarizedGet(
|
|
||||||
info: GetInfo<TParam, TResp>,
|
|
||||||
noinline body: PipelineInterceptor<Unit, ApplicationCall>
|
|
||||||
): Route =
|
|
||||||
KompendiumPreFlight.methodNotarizationPreFlight<TParam, Unit, TResp>() { paramType, requestType, responseType ->
|
|
||||||
val path = Kompendium.pathCalculator.calculate(this)
|
|
||||||
Kompendium.openApiSpec.paths.getOrPut(path) { OpenApiSpecPathItem() }
|
|
||||||
Kompendium.openApiSpec.paths[path]?.get = parseMethodInfo(info, paramType, requestType, responseType)
|
|
||||||
return method(HttpMethod.Get) { handle(body) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Notarization for an HTTP POST request
|
|
||||||
* @param TParam The class containing all parameter fields.
|
|
||||||
* Each field must be annotated with @[io.bkbn.kompendium.annotations.KompendiumField]
|
|
||||||
* @param TReq Class detailing the expected API request body
|
|
||||||
* @param TResp Class detailing the expected API response
|
|
||||||
* @param info Route metadata
|
|
||||||
*/
|
|
||||||
inline fun <reified TParam : Any, reified TReq : Any, reified TResp : Any> Route.notarizedPost(
|
|
||||||
info: PostInfo<TParam, TReq, TResp>,
|
|
||||||
noinline body: PipelineInterceptor<Unit, ApplicationCall>
|
|
||||||
): Route =
|
|
||||||
KompendiumPreFlight.methodNotarizationPreFlight<TParam, TReq, TResp>() { paramType, requestType, responseType ->
|
|
||||||
val path = Kompendium.pathCalculator.calculate(this)
|
|
||||||
Kompendium.openApiSpec.paths.getOrPut(path) { OpenApiSpecPathItem() }
|
|
||||||
Kompendium.openApiSpec.paths[path]?.post = parseMethodInfo(info, paramType, requestType, responseType)
|
|
||||||
return method(HttpMethod.Post) { handle(body) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Notarization for an HTTP Delete request
|
|
||||||
* @param TParam The class containing all parameter fields.
|
|
||||||
* Each field must be annotated with @[io.bkbn.kompendium.annotations.KompendiumField]
|
|
||||||
* @param TReq Class detailing the expected API request body
|
|
||||||
* @param TResp Class detailing the expected API response
|
|
||||||
* @param info Route metadata
|
|
||||||
*/
|
|
||||||
inline fun <reified TParam : Any, reified TReq : Any, reified TResp : Any> Route.notarizedPut(
|
|
||||||
info: PutInfo<TParam, TReq, TResp>,
|
|
||||||
noinline body: PipelineInterceptor<Unit, ApplicationCall>,
|
|
||||||
): Route =
|
|
||||||
KompendiumPreFlight.methodNotarizationPreFlight<TParam, TReq, TResp>() { paramType, requestType, responseType ->
|
|
||||||
val path = Kompendium.pathCalculator.calculate(this)
|
|
||||||
Kompendium.openApiSpec.paths.getOrPut(path) { OpenApiSpecPathItem() }
|
|
||||||
Kompendium.openApiSpec.paths[path]?.put =
|
|
||||||
parseMethodInfo(info, paramType, requestType, responseType)
|
|
||||||
return method(HttpMethod.Put) { handle(body) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Notarization for an HTTP POST request
|
|
||||||
* @param TParam The class containing all parameter fields.
|
|
||||||
* Each field must be annotated with @[io.bkbn.kompendium.annotations.KompendiumField]
|
|
||||||
* @param TResp Class detailing the expected API response
|
|
||||||
* @param info Route metadata
|
|
||||||
*/
|
|
||||||
inline fun <reified TParam : Any, reified TResp : Any> Route.notarizedDelete(
|
|
||||||
info: DeleteInfo<TParam, TResp>,
|
|
||||||
noinline body: PipelineInterceptor<Unit, ApplicationCall>
|
|
||||||
): Route =
|
|
||||||
KompendiumPreFlight.methodNotarizationPreFlight<TParam, Unit, TResp> { paramType, requestType, responseType ->
|
|
||||||
val path = Kompendium.pathCalculator.calculate(this)
|
|
||||||
Kompendium.openApiSpec.paths.getOrPut(path) { OpenApiSpecPathItem() }
|
|
||||||
Kompendium.openApiSpec.paths[path]?.delete = parseMethodInfo(info, paramType, requestType, responseType)
|
|
||||||
return method(HttpMethod.Delete) { handle(body) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Notarization for a handled exception response
|
|
||||||
* @param TErr The [Throwable] that is being handled
|
|
||||||
* @param TResp Class detailing the expected API response when handled
|
|
||||||
* @param info Response metadata
|
|
||||||
*/
|
|
||||||
inline fun <reified TErr : Throwable, reified TResp : Any> StatusPages.Configuration.notarizedException(
|
|
||||||
info: ResponseInfo<TResp>,
|
|
||||||
noinline handler: suspend PipelineContext<Unit, ApplicationCall>.(TErr) -> Unit
|
|
||||||
) = errorNotarizationPreFlight<TErr, TResp>() { errorType, responseType ->
|
|
||||||
info.parseErrorInfo(errorType, responseType)
|
|
||||||
exception(handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,12 +0,0 @@
|
|||||||
package io.bkbn.kompendium.annotations
|
|
||||||
|
|
||||||
@Retention(AnnotationRetention.RUNTIME)
|
|
||||||
@Target(AnnotationTarget.PROPERTY)
|
|
||||||
annotation class KompendiumParam(val type: ParamType, val description: String = "")
|
|
||||||
|
|
||||||
enum class ParamType {
|
|
||||||
COOKIE,
|
|
||||||
HEADER,
|
|
||||||
PATH,
|
|
||||||
QUERY
|
|
||||||
}
|
|
@ -0,0 +1,44 @@
|
|||||||
|
package io.bkbn.kompendium.core
|
||||||
|
|
||||||
|
import io.bkbn.kompendium.core.metadata.SchemaMap
|
||||||
|
import io.bkbn.kompendium.oas.OpenApiSpec
|
||||||
|
import io.bkbn.kompendium.oas.schema.TypedSchema
|
||||||
|
import io.ktor.application.Application
|
||||||
|
import io.ktor.application.ApplicationCallPipeline
|
||||||
|
import io.ktor.application.ApplicationFeature
|
||||||
|
import io.ktor.application.call
|
||||||
|
import io.ktor.http.HttpStatusCode
|
||||||
|
import io.ktor.request.path
|
||||||
|
import io.ktor.response.respond
|
||||||
|
import io.ktor.util.AttributeKey
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
class Kompendium(val config: Configuration) {
|
||||||
|
|
||||||
|
class Configuration {
|
||||||
|
lateinit var spec: OpenApiSpec
|
||||||
|
|
||||||
|
var cache: SchemaMap = emptyMap()
|
||||||
|
var specRoute = "/openapi.json"
|
||||||
|
|
||||||
|
// TODO Add tests for this!!
|
||||||
|
fun addCustomTypeSchema(clazz: KClass<*>, schema: TypedSchema) {
|
||||||
|
cache = cache.plus(clazz.simpleName!! to schema)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object Feature : ApplicationFeature<Application, Configuration, Kompendium> {
|
||||||
|
override val key: AttributeKey<Kompendium> = AttributeKey("Kompendium")
|
||||||
|
override fun install(pipeline: Application, configure: Configuration.() -> Unit): Kompendium {
|
||||||
|
val configuration = Configuration().apply(configure)
|
||||||
|
|
||||||
|
pipeline.intercept(ApplicationCallPipeline.Call) {
|
||||||
|
if (call.request.path() == configuration.specRoute) {
|
||||||
|
call.respond(HttpStatusCode.OK, configuration.spec)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Kompendium(configuration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
package io.bkbn.kompendium.core
|
||||||
|
|
||||||
|
import io.ktor.application.feature
|
||||||
|
import io.ktor.routing.Route
|
||||||
|
import io.ktor.routing.application
|
||||||
|
import kotlin.reflect.KType
|
||||||
|
import kotlin.reflect.typeOf
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Functions are considered preflight when they are used to intercept a method ahead of running.
|
||||||
|
*/
|
||||||
|
object KompendiumPreFlight {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs all content analysis on the types provided to a notarized route and adds it to the top level spec
|
||||||
|
* @param TParam
|
||||||
|
* @param TReq
|
||||||
|
* @param TResp
|
||||||
|
* @param block The function to execute, provided type information of the parameters above
|
||||||
|
* @return [Route]
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalStdlibApi::class)
|
||||||
|
inline fun <reified TParam : Any, reified TReq : Any, reified TResp : Any> Route.methodNotarizationPreFlight(
|
||||||
|
block: (KType, KType, KType) -> Route
|
||||||
|
): Route {
|
||||||
|
val feature = this.application.feature(Kompendium)
|
||||||
|
val requestType = typeOf<TReq>()
|
||||||
|
val responseType = typeOf<TResp>()
|
||||||
|
val paramType = typeOf<TParam>()
|
||||||
|
addToCache(paramType, requestType, responseType, feature)
|
||||||
|
return block.invoke(paramType, requestType, responseType)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addToCache(paramType: KType, requestType: KType, responseType: KType, feature: Kompendium) {
|
||||||
|
feature.config.cache = Kontent.generateKontent(requestType, feature.config.cache)
|
||||||
|
feature.config.cache = Kontent.generateKontent(responseType, feature.config.cache)
|
||||||
|
feature.config.cache = Kontent.generateKontent(paramType, feature.config.cache)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,467 @@
|
|||||||
|
package io.bkbn.kompendium.core
|
||||||
|
|
||||||
|
import io.bkbn.kompendium.annotations.Field
|
||||||
|
import io.bkbn.kompendium.annotations.FreeFormObject
|
||||||
|
import io.bkbn.kompendium.annotations.UndeclaredField
|
||||||
|
import io.bkbn.kompendium.annotations.constraint.Format
|
||||||
|
import io.bkbn.kompendium.annotations.constraint.MaxItems
|
||||||
|
import io.bkbn.kompendium.annotations.constraint.MaxLength
|
||||||
|
import io.bkbn.kompendium.annotations.constraint.MaxProperties
|
||||||
|
import io.bkbn.kompendium.annotations.constraint.Maximum
|
||||||
|
import io.bkbn.kompendium.annotations.constraint.MinItems
|
||||||
|
import io.bkbn.kompendium.annotations.constraint.MinLength
|
||||||
|
import io.bkbn.kompendium.annotations.constraint.MinProperties
|
||||||
|
import io.bkbn.kompendium.annotations.constraint.Minimum
|
||||||
|
import io.bkbn.kompendium.annotations.constraint.MultipleOf
|
||||||
|
import io.bkbn.kompendium.annotations.constraint.Pattern
|
||||||
|
import io.bkbn.kompendium.annotations.constraint.UniqueItems
|
||||||
|
import io.bkbn.kompendium.core.metadata.SchemaMap
|
||||||
|
import io.bkbn.kompendium.core.metadata.TypeMap
|
||||||
|
import io.bkbn.kompendium.core.util.Helpers.genericNameAdapter
|
||||||
|
import io.bkbn.kompendium.core.util.Helpers.getSimpleSlug
|
||||||
|
import io.bkbn.kompendium.core.util.Helpers.logged
|
||||||
|
import io.bkbn.kompendium.core.util.Helpers.toNumber
|
||||||
|
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 kotlin.reflect.KClass
|
||||||
|
import kotlin.reflect.KClassifier
|
||||||
|
import kotlin.reflect.KProperty1
|
||||||
|
import kotlin.reflect.KType
|
||||||
|
import kotlin.reflect.full.createType
|
||||||
|
import kotlin.reflect.full.findAnnotation
|
||||||
|
import kotlin.reflect.full.isSubclassOf
|
||||||
|
import kotlin.reflect.full.memberProperties
|
||||||
|
import kotlin.reflect.full.primaryConstructor
|
||||||
|
import kotlin.reflect.jvm.javaField
|
||||||
|
import kotlin.reflect.typeOf
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.math.BigDecimal
|
||||||
|
import java.math.BigInteger
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Responsible for generating the schema map that is used to power all object references across the API Spec.
|
||||||
|
*/
|
||||||
|
object Kontent {
|
||||||
|
|
||||||
|
private val logger = LoggerFactory.getLogger(javaClass)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Analyzes a type [T] for its top-level and any nested schemas, and adds them to a [SchemaMap], if provided
|
||||||
|
* @param T type to analyze
|
||||||
|
* @param cache Existing schema map to append to
|
||||||
|
* @return an updated schema map containing all type information for [T]
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalStdlibApi::class)
|
||||||
|
inline fun <reified T> generateKontent(
|
||||||
|
cache: SchemaMap = emptyMap()
|
||||||
|
): SchemaMap {
|
||||||
|
val kontentType = typeOf<T>()
|
||||||
|
return generateKTypeKontent(kontentType, cache)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Analyzes a [KType] for its top-level and any nested schemas, and adds them to a [SchemaMap], if provided
|
||||||
|
* @param type [KType] to analyze
|
||||||
|
* @param cache Existing schema map to append to
|
||||||
|
* @return an updated schema map containing all type information for [KType] type
|
||||||
|
*/
|
||||||
|
fun generateKontent(
|
||||||
|
type: KType,
|
||||||
|
cache: SchemaMap = emptyMap()
|
||||||
|
): SchemaMap {
|
||||||
|
var newCache = cache
|
||||||
|
gatherSubTypes(type).forEach {
|
||||||
|
newCache = generateKTypeKontent(it, newCache)
|
||||||
|
}
|
||||||
|
return newCache
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun gatherSubTypes(type: KType): List<KType> {
|
||||||
|
val classifier = type.classifier as KClass<*>
|
||||||
|
return if (classifier.isSealed) {
|
||||||
|
classifier.sealedSubclasses.map {
|
||||||
|
it.createType(type.arguments)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
listOf(type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively fills schema map depending on [KType] classifier
|
||||||
|
* @param type [KType] to parse
|
||||||
|
* @param cache Existing schema map to append to
|
||||||
|
*/
|
||||||
|
fun generateKTypeKontent(
|
||||||
|
type: KType,
|
||||||
|
cache: SchemaMap = emptyMap(),
|
||||||
|
): SchemaMap = logged(object {}.javaClass.enclosingMethod.name, mapOf("cache" to cache)) {
|
||||||
|
logger.debug("Parsing Kontent of $type")
|
||||||
|
when (val clazz = type.classifier as KClass<*>) {
|
||||||
|
Unit::class -> cache
|
||||||
|
Int::class -> cache.plus(clazz.simpleName!! to FormattedSchema("int32", "integer"))
|
||||||
|
Long::class -> cache.plus(clazz.simpleName!! to FormattedSchema("int64", "integer"))
|
||||||
|
Double::class -> cache.plus(clazz.simpleName!! to FormattedSchema("double", "number"))
|
||||||
|
Float::class -> cache.plus(clazz.simpleName!! to FormattedSchema("float", "number"))
|
||||||
|
String::class -> cache.plus(clazz.simpleName!! to SimpleSchema("string"))
|
||||||
|
Boolean::class -> cache.plus(clazz.simpleName!! to SimpleSchema("boolean"))
|
||||||
|
UUID::class -> cache.plus(clazz.simpleName!! to FormattedSchema("uuid", "string"))
|
||||||
|
BigDecimal::class -> cache.plus(clazz.simpleName!! to FormattedSchema("double", "number"))
|
||||||
|
BigInteger::class -> cache.plus(clazz.simpleName!! to FormattedSchema("int64", "integer"))
|
||||||
|
ByteArray::class -> cache.plus(clazz.simpleName!! to FormattedSchema("byte", "string"))
|
||||||
|
else -> when {
|
||||||
|
clazz.isSubclassOf(Collection::class) -> handleCollectionType(type, clazz, cache)
|
||||||
|
clazz.isSubclassOf(Enum::class) -> handleEnumType(clazz, cache)
|
||||||
|
clazz.isSubclassOf(Map::class) -> handleMapType(type, clazz, cache)
|
||||||
|
else -> handleComplexType(type, clazz, cache)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In the event of an object type, this method will parse out individual fields to recursively aggregate object map.
|
||||||
|
* @param clazz Class of the object to analyze
|
||||||
|
* @param cache Existing schema map to append to
|
||||||
|
*/
|
||||||
|
@Suppress("LongMethod", "ComplexMethod")
|
||||||
|
private fun handleComplexType(type: KType, clazz: KClass<*>, cache: SchemaMap): SchemaMap {
|
||||||
|
// This needs to be simple because it will be stored under its appropriate reference component implicitly
|
||||||
|
val slug = type.getSimpleSlug()
|
||||||
|
// Only analyze if component has not already been stored in the cache
|
||||||
|
return when (cache.containsKey(slug)) {
|
||||||
|
true -> {
|
||||||
|
logger.debug("Cache already contains $slug, returning cache untouched")
|
||||||
|
cache
|
||||||
|
}
|
||||||
|
false -> {
|
||||||
|
logger.debug("$slug was not found in cache, generating now")
|
||||||
|
var newCache = cache
|
||||||
|
// Grabs any type parameters mapped to the corresponding type argument(s)
|
||||||
|
val typeMap: TypeMap = clazz.typeParameters.zip(type.arguments).toMap()
|
||||||
|
// associates each member with a Pair of prop name to property schema
|
||||||
|
val fieldMap = clazz.memberProperties.associate { prop ->
|
||||||
|
logger.debug("Analyzing $prop in class $clazz")
|
||||||
|
// Grab the field of the current property
|
||||||
|
val field = prop.javaField?.type?.kotlin ?: error("Unable to parse field type from $prop")
|
||||||
|
// Short circuit if data is free form
|
||||||
|
val freeForm = prop.findAnnotation<FreeFormObject>()
|
||||||
|
var name = prop.name
|
||||||
|
|
||||||
|
// todo add method to clean up
|
||||||
|
when (freeForm) {
|
||||||
|
null -> {
|
||||||
|
val baseType = scanForGeneric(typeMap, prop)
|
||||||
|
val baseClazz = baseType.classifier as KClass<*>
|
||||||
|
val allTypes = scanForSealed(baseClazz, baseType)
|
||||||
|
newCache = updateCache(newCache, field, allTypes)
|
||||||
|
var propSchema = constructComponentSchema(
|
||||||
|
typeMap = typeMap,
|
||||||
|
prop = prop,
|
||||||
|
fieldClazz = field,
|
||||||
|
clazz = baseClazz,
|
||||||
|
type = baseType,
|
||||||
|
cache = newCache
|
||||||
|
)
|
||||||
|
// todo move to helper
|
||||||
|
prop.findAnnotation<Field>()?.let { fieldOverrides ->
|
||||||
|
if (fieldOverrides.description.isNotBlank()) {
|
||||||
|
propSchema = propSchema.setDescription(fieldOverrides.description)
|
||||||
|
}
|
||||||
|
if (fieldOverrides.name.isNotBlank()) {
|
||||||
|
name = fieldOverrides.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Pair(name, propSchema)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
val minProperties = prop.findAnnotation<MinProperties>()
|
||||||
|
val maxProperties = prop.findAnnotation<MaxProperties>()
|
||||||
|
val schema =
|
||||||
|
FreeFormSchema(minProperties = minProperties?.properties, maxProperties = maxProperties?.properties)
|
||||||
|
Pair(name, schema)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.debug("Looking for undeclared fields")
|
||||||
|
val undeclaredFieldMap = clazz.annotations.filterIsInstance<UndeclaredField>().associate {
|
||||||
|
val undeclaredType = it.clazz.createType()
|
||||||
|
newCache = generateKontent(undeclaredType, newCache)
|
||||||
|
it.field to newCache[undeclaredType.getSimpleSlug()]!!
|
||||||
|
}
|
||||||
|
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 { 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes the type information provided and adds any missing data to the schema map
|
||||||
|
*/
|
||||||
|
private fun updateCache(cache: SchemaMap, clazz: KClass<*>, types: List<KType>): SchemaMap {
|
||||||
|
var newCache = cache
|
||||||
|
if (!cache.containsKey(clazz.simpleName)) {
|
||||||
|
logger.debug("Cache was missing ${clazz.simpleName}, adding now")
|
||||||
|
types.forEach {
|
||||||
|
newCache = generateKTypeKontent(it, newCache)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newCache
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scans a class for sealed subclasses. If found, returns a list with all children. Otherwise, returns
|
||||||
|
* the base type
|
||||||
|
*/
|
||||||
|
private fun scanForSealed(clazz: KClass<*>, type: KType): List<KType> = if (clazz.isSealed) {
|
||||||
|
clazz.sealedSubclasses.map { it.createType(type.arguments) }
|
||||||
|
} else {
|
||||||
|
listOf(type)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Yoinks any generic types from the type map should the field be a generic
|
||||||
|
*/
|
||||||
|
private fun scanForGeneric(typeMap: TypeMap, prop: KProperty1<*, *>): KType =
|
||||||
|
if (typeMap.containsKey(prop.returnType.classifier)) {
|
||||||
|
logger.debug("Generic type detected")
|
||||||
|
typeMap[prop.returnType.classifier]?.type!!
|
||||||
|
} else {
|
||||||
|
prop.returnType
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a [ComponentSchema]
|
||||||
|
*/
|
||||||
|
private fun constructComponentSchema(
|
||||||
|
typeMap: TypeMap,
|
||||||
|
clazz: KClass<*>,
|
||||||
|
fieldClazz: KClass<*>,
|
||||||
|
prop: KProperty1<*, *>,
|
||||||
|
type: KType,
|
||||||
|
cache: SchemaMap
|
||||||
|
): ComponentSchema =
|
||||||
|
when (typeMap.containsKey(prop.returnType.classifier)) {
|
||||||
|
true -> handleGenericProperty(typeMap, clazz, type, prop.returnType.classifier, cache)
|
||||||
|
false -> handleStandardProperty(clazz, fieldClazz, prop, type, cache)
|
||||||
|
}.scanForConstraints(clazz, prop)
|
||||||
|
|
||||||
|
private fun ComponentSchema.scanForConstraints(clazz: KClass<*>, prop: KProperty1<*, *>): ComponentSchema =
|
||||||
|
when (this) {
|
||||||
|
is AnyOfSchema -> AnyOfSchema(anyOf.map { it.scanForConstraints(clazz, prop) })
|
||||||
|
is ArraySchema -> scanForConstraints(prop)
|
||||||
|
is DictionarySchema -> this // TODO Anything here?
|
||||||
|
is EnumSchema -> scanForConstraints(prop)
|
||||||
|
is FormattedSchema -> scanForConstraints(prop)
|
||||||
|
is FreeFormSchema -> this // todo anything here?
|
||||||
|
is ObjectSchema -> scanForConstraints(clazz, prop)
|
||||||
|
is SimpleSchema -> scanForConstraints(prop)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ArraySchema.scanForConstraints(prop: KProperty1<*, *>): ArraySchema {
|
||||||
|
val minItems = prop.findAnnotation<MinItems>()
|
||||||
|
val maxItems = prop.findAnnotation<MaxItems>()
|
||||||
|
val uniqueItems = prop.findAnnotation<UniqueItems>()
|
||||||
|
|
||||||
|
return this.copy(
|
||||||
|
minItems = minItems?.items,
|
||||||
|
maxItems = maxItems?.items,
|
||||||
|
uniqueItems = uniqueItems?.let { true }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun EnumSchema.scanForConstraints(prop: KProperty1<*, *>): EnumSchema {
|
||||||
|
if (prop.returnType.isMarkedNullable) {
|
||||||
|
return this.copy(nullable = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun FormattedSchema.scanForConstraints(prop: KProperty1<*, *>): FormattedSchema {
|
||||||
|
val minimum = prop.findAnnotation<Minimum>()
|
||||||
|
val maximum = prop.findAnnotation<Maximum>()
|
||||||
|
val multipleOf = prop.findAnnotation<MultipleOf>()
|
||||||
|
|
||||||
|
var schema = this
|
||||||
|
|
||||||
|
if (prop.returnType.isMarkedNullable) {
|
||||||
|
schema = schema.copy(nullable = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return schema.copy(
|
||||||
|
minimum = minimum?.min?.toNumber(),
|
||||||
|
maximum = maximum?.max?.toNumber(),
|
||||||
|
exclusiveMinimum = minimum?.exclusive,
|
||||||
|
exclusiveMaximum = maximum?.exclusive,
|
||||||
|
multipleOf = multipleOf?.multiple?.toNumber(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun SimpleSchema.scanForConstraints(prop: KProperty1<*, *>): SimpleSchema {
|
||||||
|
val minLength = prop.findAnnotation<MinLength>()
|
||||||
|
val maxLength = prop.findAnnotation<MaxLength>()
|
||||||
|
val pattern = prop.findAnnotation<Pattern>()
|
||||||
|
val format = prop.findAnnotation<Format>()
|
||||||
|
|
||||||
|
var schema = this
|
||||||
|
|
||||||
|
if (prop.returnType.isMarkedNullable) {
|
||||||
|
schema = schema.copy(nullable = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return schema.copy(
|
||||||
|
minLength = minLength?.length,
|
||||||
|
maxLength = maxLength?.length,
|
||||||
|
pattern = pattern?.pattern,
|
||||||
|
format = format?.format
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ObjectSchema.scanForConstraints(clazz: KClass<*>, prop: KProperty1<*, *>): ObjectSchema {
|
||||||
|
val requiredParams = clazz.primaryConstructor?.parameters?.filterNot { it.isOptional } ?: emptyList()
|
||||||
|
var schema = this
|
||||||
|
|
||||||
|
// todo dedup this
|
||||||
|
if (requiredParams.isNotEmpty()) {
|
||||||
|
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) {
|
||||||
|
schema = schema.copy(nullable = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return schema
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If a field has no type parameters, build its [ComponentSchema] without referencing the [TypeMap]
|
||||||
|
*/
|
||||||
|
private fun handleStandardProperty(
|
||||||
|
clazz: KClass<*>,
|
||||||
|
fieldClazz: KClass<*>,
|
||||||
|
prop: KProperty1<*, *>,
|
||||||
|
type: KType,
|
||||||
|
cache: SchemaMap
|
||||||
|
): ComponentSchema = if (clazz.isSealed) {
|
||||||
|
val refs = clazz.sealedSubclasses
|
||||||
|
.map { it.createType(type.arguments) }
|
||||||
|
.map { cache[it.getSimpleSlug()] ?: error("$it not found in cache") }
|
||||||
|
AnyOfSchema(refs)
|
||||||
|
} else {
|
||||||
|
val slug = fieldClazz.getSimpleSlug(prop)
|
||||||
|
cache[slug] ?: error("$slug not found in cache")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If a field has type parameters, leverage the constructed [TypeMap] to construct the [ComponentSchema]
|
||||||
|
*/
|
||||||
|
private fun handleGenericProperty(
|
||||||
|
typeMap: TypeMap,
|
||||||
|
clazz: KClass<*>,
|
||||||
|
type: KType,
|
||||||
|
classifier: KClassifier?,
|
||||||
|
cache: SchemaMap
|
||||||
|
): ComponentSchema = if (clazz.isSealed) {
|
||||||
|
val refs = clazz.sealedSubclasses
|
||||||
|
.map { it.createType(type.arguments) }
|
||||||
|
.map { it.getSimpleSlug() }
|
||||||
|
.map { cache[it] ?: error("$it not available in cache") }
|
||||||
|
AnyOfSchema(refs)
|
||||||
|
} else {
|
||||||
|
val slug = typeMap[classifier]?.type!!.getSimpleSlug()
|
||||||
|
cache[slug] ?: error("$slug not found in cache")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for when an [Enum] is encountered
|
||||||
|
* @param clazz Class of the object to analyze
|
||||||
|
* @param cache Existing schema map to append to
|
||||||
|
*/
|
||||||
|
private fun handleEnumType(clazz: KClass<*>, cache: SchemaMap): SchemaMap {
|
||||||
|
val options = clazz.java.enumConstants.map { it.toString() }.toSet()
|
||||||
|
return cache.plus(clazz.simpleName!! to EnumSchema(options))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for when a [Map] is encountered
|
||||||
|
* @param type Map type information
|
||||||
|
* @param clazz Map class information
|
||||||
|
* @param cache Existing schema map to append to
|
||||||
|
*/
|
||||||
|
private fun handleMapType(type: KType, clazz: KClass<*>, cache: SchemaMap): SchemaMap {
|
||||||
|
logger.debug("Map detected for $type, generating schema and appending to cache")
|
||||||
|
val (keyType, valType) = type.arguments.map { it.type }
|
||||||
|
logger.debug("Obtained map types -> key: $keyType and value: $valType")
|
||||||
|
if (keyType?.classifier != String::class) {
|
||||||
|
error("Invalid Map $type: OpenAPI dictionaries must have keys of type String")
|
||||||
|
}
|
||||||
|
var updatedCache = generateKTypeKontent(valType!!, cache)
|
||||||
|
val valClass = valType.classifier as KClass<*>
|
||||||
|
val valClassName = valClass.simpleName
|
||||||
|
val referenceName = genericNameAdapter(type, clazz)
|
||||||
|
val valueReference = when (valClass.isSealed) {
|
||||||
|
true -> {
|
||||||
|
val subTypes = gatherSubTypes(valType)
|
||||||
|
AnyOfSchema(subTypes.map {
|
||||||
|
updatedCache = generateKTypeKontent(it, updatedCache)
|
||||||
|
updatedCache[it.getSimpleSlug()] ?: error("${it.getSimpleSlug()} not found")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
false -> updatedCache[valClassName] ?: error("$valClassName not found")
|
||||||
|
}
|
||||||
|
val schema = DictionarySchema(additionalProperties = valueReference)
|
||||||
|
updatedCache = generateKontent(valType, updatedCache)
|
||||||
|
return updatedCache.plus(referenceName to schema)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for when a [Collection] is encountered
|
||||||
|
* @param type Collection type information
|
||||||
|
* @param clazz Collection class information
|
||||||
|
* @param cache Existing schema map to append to
|
||||||
|
*/
|
||||||
|
private fun handleCollectionType(type: KType, clazz: KClass<*>, cache: SchemaMap): SchemaMap {
|
||||||
|
logger.debug("Collection detected for $type, generating schema and appending to cache")
|
||||||
|
val collectionType = type.arguments.first().type!!
|
||||||
|
val collectionClass = collectionType.classifier as KClass<*>
|
||||||
|
logger.debug("Obtained collection class: $collectionClass")
|
||||||
|
val referenceName = genericNameAdapter(type, clazz)
|
||||||
|
var updatedCache = generateKTypeKontent(collectionType, cache)
|
||||||
|
val valueReference = when (collectionClass.isSealed) {
|
||||||
|
true -> {
|
||||||
|
val subTypes = gatherSubTypes(collectionType)
|
||||||
|
AnyOfSchema(subTypes.map {
|
||||||
|
updatedCache = generateKTypeKontent(it, cache)
|
||||||
|
updatedCache[it.getSimpleSlug()] ?: error("${it.getSimpleSlug()} not found")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
false -> updatedCache[collectionClass.simpleName] ?: error("${collectionClass.simpleName} not found")
|
||||||
|
}
|
||||||
|
val schema = ArraySchema(items = valueReference)
|
||||||
|
updatedCache = generateKontent(collectionType, cache)
|
||||||
|
return updatedCache.plus(referenceName to schema)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,172 @@
|
|||||||
|
package io.bkbn.kompendium.core
|
||||||
|
|
||||||
|
import io.bkbn.kompendium.annotations.Param
|
||||||
|
import io.bkbn.kompendium.core.KompendiumPreFlight.methodNotarizationPreFlight
|
||||||
|
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
|
||||||
|
import io.ktor.application.feature
|
||||||
|
import io.ktor.http.HttpMethod
|
||||||
|
import io.ktor.routing.Route
|
||||||
|
import io.ktor.routing.application
|
||||||
|
import io.ktor.routing.method
|
||||||
|
import io.ktor.util.pipeline.PipelineInterceptor
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notarization methods are the primary way that a Ktor API using Kompendium differentiates
|
||||||
|
* from a default Ktor application. On instantiation, a notarized route, provided with the proper metadata,
|
||||||
|
* will reflectively analyze all pertinent data to build a corresponding OpenAPI entry.
|
||||||
|
*/
|
||||||
|
object Notarized {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notarization for an HTTP GET 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.notarizedGet(
|
||||||
|
info: GetInfo<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]?.get = postProcess(baseInfo)
|
||||||
|
return method(HttpMethod.Get) { handle(body) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notarization for an HTTP POST 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.notarizedPost(
|
||||||
|
info: PostInfo<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]?.post = postProcess(baseInfo)
|
||||||
|
return method(HttpMethod.Post) { handle(body) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
* @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.notarizedPut(
|
||||||
|
info: PutInfo<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]?.put = postProcess(baseInfo)
|
||||||
|
return method(HttpMethod.Put) { handle(body) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
* @param postProcess Adds an optional callback hook to perform manual overrides on the generated [PathOperation]
|
||||||
|
*/
|
||||||
|
inline fun <reified TParam : Any, reified TResp : Any> Route.notarizedDelete(
|
||||||
|
info: DeleteInfo<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]?.delete = postProcess(baseInfo)
|
||||||
|
return method(HttpMethod.Delete) { handle(body) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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]
|
||||||
|
*/
|
||||||
|
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,12 @@
|
|||||||
|
package io.bkbn.kompendium.core.metadata
|
||||||
|
|
||||||
|
import io.ktor.http.HttpStatusCode
|
||||||
|
import kotlin.reflect.KType
|
||||||
|
|
||||||
|
data class ExceptionInfo<TResp : Any>(
|
||||||
|
val responseType: KType,
|
||||||
|
val status: HttpStatusCode,
|
||||||
|
val description: String,
|
||||||
|
val mediaTypes: List<String> = listOf("application/json"),
|
||||||
|
val examples: Map<String, TResp> = emptyMap()
|
||||||
|
)
|
@ -0,0 +1,3 @@
|
|||||||
|
package io.bkbn.kompendium.core.metadata
|
||||||
|
|
||||||
|
data class ParameterExample(val parameterName: String, val exampleName: String, val exampleValue: Any)
|
@ -1,4 +1,4 @@
|
|||||||
package io.bkbn.kompendium.models.meta
|
package io.bkbn.kompendium.core.metadata
|
||||||
|
|
||||||
data class RequestInfo<TReq>(
|
data class RequestInfo<TReq>(
|
||||||
val description: String,
|
val description: String,
|
@ -1,4 +1,4 @@
|
|||||||
package io.bkbn.kompendium.models.meta
|
package io.bkbn.kompendium.core.metadata
|
||||||
|
|
||||||
import io.ktor.http.HttpStatusCode
|
import io.ktor.http.HttpStatusCode
|
||||||
|
|
@ -0,0 +1,5 @@
|
|||||||
|
package io.bkbn.kompendium.core.metadata
|
||||||
|
|
||||||
|
import io.bkbn.kompendium.oas.schema.ComponentSchema
|
||||||
|
|
||||||
|
typealias SchemaMap = Map<String, ComponentSchema>
|
@ -0,0 +1,6 @@
|
|||||||
|
package io.bkbn.kompendium.core.metadata
|
||||||
|
|
||||||
|
import kotlin.reflect.KTypeParameter
|
||||||
|
import kotlin.reflect.KTypeProjection
|
||||||
|
|
||||||
|
typealias TypeMap = Map<KTypeParameter, KTypeProjection>
|
@ -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 DeleteInfo<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,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 GetInfo<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,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>
|
@ -0,0 +1,24 @@
|
|||||||
|
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> {
|
||||||
|
val summary: String
|
||||||
|
val description: String?
|
||||||
|
get() = null
|
||||||
|
val tags: Set<String>
|
||||||
|
get() = emptySet()
|
||||||
|
val deprecated: Boolean
|
||||||
|
get() = false
|
||||||
|
val securitySchemes: Set<String>
|
||||||
|
get() = emptySet()
|
||||||
|
val canThrow: Set<ExceptionInfo<*>>
|
||||||
|
get() = emptySet()
|
||||||
|
val responseInfo: ResponseInfo<TResp>
|
||||||
|
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>
|
@ -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 PostInfo<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>
|
@ -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 PutInfo<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>
|
@ -0,0 +1,3 @@
|
|||||||
|
package io.bkbn.kompendium.core.parser
|
||||||
|
|
||||||
|
object DefaultMethodParser : IMethodParser
|
@ -0,0 +1,252 @@
|
|||||||
|
package io.bkbn.kompendium.core.parser
|
||||||
|
|
||||||
|
import io.bkbn.kompendium.annotations.Param
|
||||||
|
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
|
||||||
|
import io.bkbn.kompendium.core.metadata.method.PostInfo
|
||||||
|
import io.bkbn.kompendium.core.metadata.method.PutInfo
|
||||||
|
import io.bkbn.kompendium.core.util.Helpers
|
||||||
|
import io.bkbn.kompendium.core.util.Helpers.capitalized
|
||||||
|
import io.bkbn.kompendium.core.util.Helpers.getSimpleSlug
|
||||||
|
import io.bkbn.kompendium.oas.path.PathOperation
|
||||||
|
import io.bkbn.kompendium.oas.payload.MediaType
|
||||||
|
import io.bkbn.kompendium.oas.payload.Parameter
|
||||||
|
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
|
||||||
|
|
||||||
|
interface IMethodParser {
|
||||||
|
/**
|
||||||
|
* Generates the OpenAPI Path spec from provided metadata
|
||||||
|
* @param info implementation of the [MethodInfo] sealed class
|
||||||
|
* @param paramType Type of `TParam`
|
||||||
|
* @param requestType Type of `TReq` if required
|
||||||
|
* @param responseType Type of `TResp`
|
||||||
|
* @return object representing the OpenAPI Path spec.
|
||||||
|
*/
|
||||||
|
fun parseMethodInfo(
|
||||||
|
info: MethodInfo<*, *>,
|
||||||
|
paramType: KType,
|
||||||
|
requestType: KType,
|
||||||
|
responseType: KType,
|
||||||
|
feature: Kompendium
|
||||||
|
) = PathOperation(
|
||||||
|
summary = info.summary,
|
||||||
|
description = info.description,
|
||||||
|
operationId = info.operationId,
|
||||||
|
tags = info.tags,
|
||||||
|
deprecated = info.deprecated,
|
||||||
|
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)
|
||||||
|
is PostInfo<*, *, *> -> requestType.toRequestSpec(info.requestInfo, feature)
|
||||||
|
else -> null
|
||||||
|
},
|
||||||
|
security = if (info.securitySchemes.isNotEmpty()) listOf(
|
||||||
|
// TODO support scopes
|
||||||
|
info.securitySchemes.associateWith { listOf() }
|
||||||
|
) else null
|
||||||
|
)
|
||||||
|
|
||||||
|
fun parseResponse(
|
||||||
|
responseType: KType,
|
||||||
|
responseInfo: ResponseInfo<*>?,
|
||||||
|
feature: Kompendium
|
||||||
|
): Map<Int, Response> = responseType.toResponseSpec(responseInfo, feature)?.let { mapOf(it) }.orEmpty()
|
||||||
|
|
||||||
|
fun parseExceptions(
|
||||||
|
exceptionInfo: Set<ExceptionInfo<*>>,
|
||||||
|
feature: Kompendium,
|
||||||
|
): 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)
|
||||||
|
)
|
||||||
|
Pair(info.status.value, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a [KType] to an [Request]
|
||||||
|
* @receiver [KType] to convert
|
||||||
|
* @param requestInfo request metadata
|
||||||
|
* @return Will return a generated [Request] if requestInfo is not null
|
||||||
|
*/
|
||||||
|
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 as Map<String, Any>)
|
||||||
|
?: mapOf(),
|
||||||
|
required = requestInfo.required
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a [KType] to a pairing of http status code to [Response]
|
||||||
|
* @receiver [KType] to convert
|
||||||
|
* @param responseInfo response metadata
|
||||||
|
* @return Will return a generated [Pair] if responseInfo is not null
|
||||||
|
*/
|
||||||
|
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 as Map<String, Any>)
|
||||||
|
)
|
||||||
|
Pair(responseInfo.status.value, specResponse)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates MediaTypes along with any examples provided
|
||||||
|
* @param type [KType] Type of the object
|
||||||
|
* @param mediaTypes list of acceptable http media types
|
||||||
|
* @param examples Mapping of named examples of valid bodies.
|
||||||
|
* @return Named mapping of media types.
|
||||||
|
*/
|
||||||
|
fun Kompendium.resolveContent(
|
||||||
|
type: KType,
|
||||||
|
mediaTypes: List<String>,
|
||||||
|
examples: Map<String, Any>
|
||||||
|
): Map<String, MediaType>? {
|
||||||
|
val classifier = type.classifier as KClass<*>
|
||||||
|
return if (type != Helpers.UNIT_TYPE && mediaTypes.isNotEmpty()) {
|
||||||
|
mediaTypes.associateWith {
|
||||||
|
val schema = if (classifier.isSealed) {
|
||||||
|
val refs = classifier.sealedSubclasses
|
||||||
|
.map { it.createType(type.arguments) }
|
||||||
|
.map { it.getSimpleSlug() }
|
||||||
|
.map { config.cache[it] ?: error("$it not available") }
|
||||||
|
AnyOfSchema(refs)
|
||||||
|
} else {
|
||||||
|
val ref = type.getSimpleSlug()
|
||||||
|
config.cache[ref] ?: error("$ref not available")
|
||||||
|
}
|
||||||
|
MediaType(
|
||||||
|
schema = schema,
|
||||||
|
examples = examples.mapValues { (_, v) -> MediaType.Example(v) }.ifEmpty { null }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a type for all parameter information. All fields in the receiver
|
||||||
|
* must be annotated with [io.bkbn.kompendium.annotations.Param].
|
||||||
|
* @receiver type
|
||||||
|
* @return list of valid parameter specs as detailed by the [KType] members
|
||||||
|
* @throws [IllegalStateException] if the class could not be parsed properly
|
||||||
|
*/
|
||||||
|
fun KType.toParameterSpec(info: MethodInfo<*, *>, feature: Kompendium): List<Parameter> {
|
||||||
|
val clazz = classifier as KClass<*>
|
||||||
|
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) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Absolutely disgusting reflection to determine if a default value is available for a given property.
|
||||||
|
* @param clazz to which the property belongs
|
||||||
|
* @param prop the property in question
|
||||||
|
* @return The default value if found
|
||||||
|
*/
|
||||||
|
fun getDefaultParameterValue(clazz: KClass<*>, prop: KProperty<*>): Any? {
|
||||||
|
val constructor = clazz.primaryConstructor
|
||||||
|
val parameterInQuestion = constructor
|
||||||
|
?.parameters
|
||||||
|
?.find { it.name == prop.name }
|
||||||
|
?: error("could not find parameter ${prop.name}")
|
||||||
|
if (!parameterInQuestion.isOptional) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val values = constructor
|
||||||
|
.parameters
|
||||||
|
.filterNot { it.isOptional }
|
||||||
|
.associateWith { defaultValueInjector(it) }
|
||||||
|
val instance = constructor.callBy(values)
|
||||||
|
val methods = clazz.java.methods
|
||||||
|
val getterName = "get${prop.name.capitalized()}"
|
||||||
|
val getterFunction = methods.find { it.name == getterName }
|
||||||
|
?: error("Could not associate ${prop.name} with a getter")
|
||||||
|
return getterFunction.invoke(instance)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows the reflection invoker to populate a parameter map with values in order to sus out any default parameters.
|
||||||
|
* @param param Parameter to provide value for
|
||||||
|
* @return value of the proper type to match param
|
||||||
|
* @throws [IllegalStateException] if parameter type is not one of the basic types supported below.
|
||||||
|
*/
|
||||||
|
fun defaultValueInjector(param: KParameter): Any = when (param.type.classifier) {
|
||||||
|
String::class -> "test"
|
||||||
|
Boolean::class -> false
|
||||||
|
Int::class -> 1
|
||||||
|
Long::class -> 2
|
||||||
|
Double::class -> 1.0
|
||||||
|
Float::class -> 1.0
|
||||||
|
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("/\\(.+\\)"), "")
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package io.bkbn.kompendium.routes
|
package io.bkbn.kompendium.core.routes
|
||||||
|
|
||||||
import io.ktor.application.call
|
import io.ktor.application.call
|
||||||
import io.ktor.html.respondHtml
|
import io.ktor.html.respondHtml
|
||||||
@ -13,20 +13,19 @@ import kotlinx.html.script
|
|||||||
import kotlinx.html.style
|
import kotlinx.html.style
|
||||||
import kotlinx.html.title
|
import kotlinx.html.title
|
||||||
import kotlinx.html.unsafe
|
import kotlinx.html.unsafe
|
||||||
import io.bkbn.kompendium.models.oas.OpenApiSpec
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides an out-of-the-box route to view docs using ReDoc
|
* Provides an out-of-the-box route to view docs using ReDoc
|
||||||
* @param oas spec to reference
|
* @param pageTitle Webpage title you wish to be displayed on your docs
|
||||||
* @param specUrl url to point ReDoc to the OpenAPI json document
|
* @param specUrl url to point ReDoc to the OpenAPI json document
|
||||||
*/
|
*/
|
||||||
fun Routing.redoc(oas: OpenApiSpec, specUrl: String = "/openapi.json") {
|
fun Routing.redoc(pageTitle: String = "Docs", specUrl: String = "/openapi.json") {
|
||||||
route("/docs") {
|
route("/docs") {
|
||||||
get {
|
get {
|
||||||
call.respondHtml {
|
call.respondHtml {
|
||||||
head {
|
head {
|
||||||
title {
|
title {
|
||||||
+"${oas.info.title}"
|
+"$pageTitle"
|
||||||
}
|
}
|
||||||
meta {
|
meta {
|
||||||
charset = "utf-8"
|
charset = "utf-8"
|
@ -1,6 +1,5 @@
|
|||||||
package io.bkbn.kompendium.util
|
package io.bkbn.kompendium.core.util
|
||||||
|
|
||||||
import io.bkbn.kompendium.util.Helpers.getReferenceSlug
|
|
||||||
import java.lang.reflect.ParameterizedType
|
import java.lang.reflect.ParameterizedType
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
import kotlin.reflect.KProperty
|
import kotlin.reflect.KProperty
|
||||||
@ -8,17 +7,18 @@ import kotlin.reflect.KType
|
|||||||
import kotlin.reflect.full.createType
|
import kotlin.reflect.full.createType
|
||||||
import kotlin.reflect.jvm.javaField
|
import kotlin.reflect.jvm.javaField
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
object Helpers {
|
object Helpers {
|
||||||
|
|
||||||
private val logger = LoggerFactory.getLogger(javaClass)
|
private val logger = LoggerFactory.getLogger(javaClass)
|
||||||
|
|
||||||
const val COMPONENT_SLUG = "#/components/schemas"
|
private const val COMPONENT_SLUG = "#/components/schemas"
|
||||||
|
|
||||||
val UNIT_TYPE by lazy { Unit::class.createType() }
|
val UNIT_TYPE by lazy { Unit::class.createType() }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Higher order function that takes a map of names to objects and will log their state ahead of function invocation
|
* Higher order function that takes a map of names to object and will log their state ahead of function invocation
|
||||||
* along with the result of the function invocation
|
* along with the result of the function invocation
|
||||||
*/
|
*/
|
||||||
fun <T> logged(functionName: String, entities: Map<String, Any>, block: () -> T): T {
|
fun <T> logged(functionName: String, entities: Map<String, Any>, block: () -> T): T {
|
||||||
@ -70,4 +70,16 @@ object Helpers {
|
|||||||
.map { it.simpleName }
|
.map { it.simpleName }
|
||||||
return classNames.joinToString(separator = "-", prefix = "${clazz.simpleName}-")
|
return classNames.joinToString(separator = "-", prefix = "${clazz.simpleName}-")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun String.capitalized() = replaceFirstChar {
|
||||||
|
if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun String.toNumber(): Number {
|
||||||
|
return try {
|
||||||
|
this.toInt()
|
||||||
|
} catch (e: NumberFormatException) {
|
||||||
|
this.toDouble()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,6 +0,0 @@
|
|||||||
package io.bkbn.kompendium.models.meta
|
|
||||||
|
|
||||||
import kotlin.reflect.KType
|
|
||||||
import io.bkbn.kompendium.models.oas.OpenApiSpecResponse
|
|
||||||
|
|
||||||
typealias ErrorMap = Map<KType, Pair<Int, OpenApiSpecResponse<*>>?>
|
|
@ -1,95 +0,0 @@
|
|||||||
package io.bkbn.kompendium.models.meta
|
|
||||||
|
|
||||||
import kotlin.reflect.KClass
|
|
||||||
|
|
||||||
sealed class MethodInfo<TParam, TResp>(
|
|
||||||
open val summary: String,
|
|
||||||
open val description: String? = null,
|
|
||||||
open val tags: Set<String> = emptySet(),
|
|
||||||
open val deprecated: Boolean = false,
|
|
||||||
open val securitySchemes: Set<String> = emptySet(),
|
|
||||||
open val canThrow: Set<KClass<*>> = emptySet(),
|
|
||||||
open val responseInfo: ResponseInfo<TResp>? = null,
|
|
||||||
open val parameterExamples: Map<String, TParam> = emptyMap(),
|
|
||||||
) {
|
|
||||||
|
|
||||||
data class GetInfo<TParam, TResp>(
|
|
||||||
override val responseInfo: ResponseInfo<TResp>? = null,
|
|
||||||
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<KClass<*>> = emptySet(),
|
|
||||||
override val parameterExamples: Map<String, TParam> = emptyMap()
|
|
||||||
) : MethodInfo<TParam, TResp>(
|
|
||||||
summary = summary,
|
|
||||||
description = description,
|
|
||||||
tags = tags,
|
|
||||||
deprecated = deprecated,
|
|
||||||
securitySchemes = securitySchemes,
|
|
||||||
canThrow = canThrow,
|
|
||||||
responseInfo = responseInfo,
|
|
||||||
parameterExamples = parameterExamples
|
|
||||||
)
|
|
||||||
|
|
||||||
data class PostInfo<TParam, TReq, TResp>(
|
|
||||||
val requestInfo: RequestInfo<TReq>? = null,
|
|
||||||
override val responseInfo: ResponseInfo<TResp>? = null,
|
|
||||||
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<KClass<*>> = emptySet(),
|
|
||||||
override val parameterExamples: Map<String, TParam> = emptyMap()
|
|
||||||
) : MethodInfo<TParam, TResp>(
|
|
||||||
summary = summary,
|
|
||||||
description = description,
|
|
||||||
tags = tags,
|
|
||||||
deprecated = deprecated,
|
|
||||||
securitySchemes = securitySchemes,
|
|
||||||
canThrow = canThrow,
|
|
||||||
responseInfo = responseInfo,
|
|
||||||
parameterExamples = parameterExamples
|
|
||||||
)
|
|
||||||
|
|
||||||
data class PutInfo<TParam, TReq, TResp>(
|
|
||||||
val requestInfo: RequestInfo<TReq>? = null,
|
|
||||||
override val responseInfo: ResponseInfo<TResp>? = null,
|
|
||||||
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<KClass<*>> = emptySet(),
|
|
||||||
override val parameterExamples: Map<String, TParam> = emptyMap()
|
|
||||||
) : MethodInfo<TParam, TResp>(
|
|
||||||
summary = summary,
|
|
||||||
description = description,
|
|
||||||
tags = tags,
|
|
||||||
deprecated = deprecated,
|
|
||||||
securitySchemes = securitySchemes,
|
|
||||||
canThrow = canThrow,
|
|
||||||
parameterExamples = parameterExamples
|
|
||||||
)
|
|
||||||
|
|
||||||
data class DeleteInfo<TParam, TResp>(
|
|
||||||
override val responseInfo: ResponseInfo<TResp>? = null,
|
|
||||||
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<KClass<*>> = emptySet(),
|
|
||||||
override val parameterExamples: Map<String, TParam> = emptyMap()
|
|
||||||
) : MethodInfo<TParam, TResp>(
|
|
||||||
summary = summary,
|
|
||||||
description = description,
|
|
||||||
tags = tags,
|
|
||||||
deprecated = deprecated,
|
|
||||||
securitySchemes = securitySchemes,
|
|
||||||
canThrow = canThrow,
|
|
||||||
parameterExamples = parameterExamples
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,5 +0,0 @@
|
|||||||
package io.bkbn.kompendium.models.meta
|
|
||||||
|
|
||||||
import io.bkbn.kompendium.models.oas.OpenApiSpecComponentSchema
|
|
||||||
|
|
||||||
typealias SchemaMap = Map<String, OpenApiSpecComponentSchema>
|
|
@ -1,14 +0,0 @@
|
|||||||
package io.bkbn.kompendium.models.oas
|
|
||||||
|
|
||||||
data class OpenApiSpec(
|
|
||||||
val openapi: String = "3.0.3",
|
|
||||||
val info: OpenApiSpecInfo,
|
|
||||||
// TODO Needs to default to server object with url of `/`
|
|
||||||
val servers: MutableList<OpenApiSpecServer> = mutableListOf(),
|
|
||||||
val paths: MutableMap<String, OpenApiSpecPathItem> = mutableMapOf(),
|
|
||||||
val components: OpenApiSpecComponents = OpenApiSpecComponents(),
|
|
||||||
// todo needs to reference objects in the components -> security scheme 🤔
|
|
||||||
val security: MutableList<Map<String, List<String>>> = mutableListOf(),
|
|
||||||
val tags: MutableList<OpenApiSpecTag> = mutableListOf(),
|
|
||||||
val externalDocs: OpenApiSpecExternalDocumentation? = null
|
|
||||||
)
|
|
@ -1,44 +0,0 @@
|
|||||||
package io.bkbn.kompendium.models.oas
|
|
||||||
|
|
||||||
sealed class OpenApiSpecComponentSchema(open val default: Any? = null) {
|
|
||||||
|
|
||||||
fun addDefault(default: Any?): OpenApiSpecComponentSchema = when (this) {
|
|
||||||
is AnyOfReferencedSchema -> error("Cannot add default to anyOf reference")
|
|
||||||
is ReferencedSchema -> this.copy(default = default)
|
|
||||||
is ObjectSchema -> this.copy(default = default)
|
|
||||||
is DictionarySchema -> this.copy(default = default)
|
|
||||||
is EnumSchema -> this.copy(default = default)
|
|
||||||
is SimpleSchema -> this.copy(default = default)
|
|
||||||
is FormatSchema -> this.copy(default = default)
|
|
||||||
is ArraySchema -> this.copy(default = default)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed class TypedSchema(open val type: String, override val default: Any? = null) : OpenApiSpecComponentSchema(default)
|
|
||||||
|
|
||||||
data class ReferencedSchema(val `$ref`: String, override val default: Any? = null) : OpenApiSpecComponentSchema(default)
|
|
||||||
data class AnyOfReferencedSchema(val anyOf: List<ReferencedSchema>) : OpenApiSpecComponentSchema()
|
|
||||||
|
|
||||||
data class ObjectSchema(
|
|
||||||
val properties: Map<String, OpenApiSpecComponentSchema>,
|
|
||||||
override val default: Any? = null
|
|
||||||
) : TypedSchema("object", default)
|
|
||||||
|
|
||||||
data class DictionarySchema(
|
|
||||||
val additionalProperties: OpenApiSpecComponentSchema,
|
|
||||||
override val default: Any? = null
|
|
||||||
) : TypedSchema("object", default)
|
|
||||||
|
|
||||||
data class EnumSchema(
|
|
||||||
val `enum`: Set<String>, override val default: Any? = null
|
|
||||||
) : TypedSchema("string", default)
|
|
||||||
|
|
||||||
data class SimpleSchema(override val type: String, override val default: Any? = null) : TypedSchema(type, default)
|
|
||||||
|
|
||||||
data class FormatSchema(val format: String, override val type: String, override val default: Any? = null) :
|
|
||||||
TypedSchema(type, default)
|
|
||||||
|
|
||||||
data class ArraySchema(val items: OpenApiSpecComponentSchema, override val default: Any? = null) :
|
|
||||||
TypedSchema("array", default)
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
|||||||
package io.bkbn.kompendium.models.oas
|
|
||||||
|
|
||||||
// TODO I *think* the only thing I need here is the security https://swagger.io/specification/#components-object
|
|
||||||
data class OpenApiSpecComponents(
|
|
||||||
val schemas: MutableMap<String, OpenApiSpecComponentSchema> = mutableMapOf(),
|
|
||||||
val securitySchemes: MutableMap<String, OpenApiSpecSchemaSecurity> = mutableMapOf()
|
|
||||||
)
|
|
@ -1,8 +0,0 @@
|
|||||||
package io.bkbn.kompendium.models.oas
|
|
||||||
|
|
||||||
import java.net.URI
|
|
||||||
|
|
||||||
data class OpenApiSpecExternalDocumentation(
|
|
||||||
val url: URI,
|
|
||||||
val description: String?
|
|
||||||
)
|
|
@ -1,12 +0,0 @@
|
|||||||
package io.bkbn.kompendium.models.oas
|
|
||||||
|
|
||||||
import java.net.URI
|
|
||||||
|
|
||||||
data class OpenApiSpecInfo(
|
|
||||||
var title: String? = null,
|
|
||||||
var version: String? = null,
|
|
||||||
var description: String? = null,
|
|
||||||
var termsOfService: URI? = null,
|
|
||||||
var contact: OpenApiSpecInfoContact? = null,
|
|
||||||
var license: OpenApiSpecInfoLicense? = null
|
|
||||||
)
|
|
@ -1,9 +0,0 @@
|
|||||||
package io.bkbn.kompendium.models.oas
|
|
||||||
|
|
||||||
import java.net.URI
|
|
||||||
|
|
||||||
data class OpenApiSpecInfoContact(
|
|
||||||
var name: String,
|
|
||||||
var url: URI? = null,
|
|
||||||
var email: String? = null // TODO Enforce email?
|
|
||||||
)
|
|
@ -1,8 +0,0 @@
|
|||||||
package io.bkbn.kompendium.models.oas
|
|
||||||
|
|
||||||
import java.net.URI
|
|
||||||
|
|
||||||
data class OpenApiSpecInfoLicense(
|
|
||||||
var name: String,
|
|
||||||
var url: URI? = null
|
|
||||||
)
|
|
@ -1,10 +0,0 @@
|
|||||||
package io.bkbn.kompendium.models.oas
|
|
||||||
|
|
||||||
data class OpenApiSpecLink(
|
|
||||||
val operationRef: String?, // todo mutually exclusive with operationId
|
|
||||||
val operationId: String?,
|
|
||||||
val parameters: Map<String, String>, // todo sheesh https://swagger.io/specification/#link-object
|
|
||||||
val requestBody: String, // todo same
|
|
||||||
val description: String?,
|
|
||||||
val server: OpenApiSpecServer?
|
|
||||||
)
|
|
@ -1,8 +0,0 @@
|
|||||||
package io.bkbn.kompendium.models.oas
|
|
||||||
|
|
||||||
data class OpenApiSpecMediaType<T>(
|
|
||||||
val schema: OpenApiSpecReferencable,
|
|
||||||
val examples: Map<String, ExampleWrapper<T>>? = null
|
|
||||||
)
|
|
||||||
|
|
||||||
data class ExampleWrapper<T>(val value: T)
|
|
@ -1,10 +0,0 @@
|
|||||||
package io.bkbn.kompendium.models.oas
|
|
||||||
|
|
||||||
import java.net.URI
|
|
||||||
|
|
||||||
data class OpenApiSpecOAuthFlow(
|
|
||||||
val authorizationUrl: URI? = null,
|
|
||||||
val tokenUrl: URI? = null,
|
|
||||||
val refreshUrl: URI? = null,
|
|
||||||
val scopes: Map<String, String>? = null
|
|
||||||
)
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user