Compare commits
52 Commits
Author | SHA1 | Date | |
---|---|---|---|
3c585c06a3 | |||
7bfd168d74 | |||
19298f4deb | |||
54bdf107e2 | |||
721302d651 | |||
3ffda43a52 | |||
ae2a1b578a | |||
147c7e7fb0 | |||
906b329c2e | |||
1bf81cfd82 | |||
c9f173d6b0 | |||
c43fafae1b | |||
3b2fa72d26 | |||
91d4ec10b7 | |||
aa1b898b22 | |||
aa21c1219b | |||
bc380077fb | |||
fc9929e9cc | |||
a26ad72b67 | |||
38a70e4979 | |||
4a1425b73b | |||
01c118373e | |||
7535d67661 | |||
eb369dcdc8 | |||
da104d0a63 | |||
c6ed261fe4 | |||
012db5ad26 | |||
f02f7ad211 | |||
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
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up JDK 11
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
distribution: 'adopt'
|
||||
java-version: '11'
|
||||
- name: Cache Gradle packages
|
||||
uses: actions/cache@v2
|
||||
java-version: '17'
|
||||
- name: Lint
|
||||
uses: burrunan/gradle-cache-action@v1
|
||||
with:
|
||||
path: ~/.gradle/caches
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle.kts') }}
|
||||
restore-keys: ${{ runner.os }}-gradle
|
||||
- name: Lint using detekt
|
||||
run: ./gradlew detekt
|
||||
gradle-version: wrapper
|
||||
arguments: detekt
|
||||
unit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up JDK 11
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
distribution: 'adopt'
|
||||
java-version: '11'
|
||||
- name: Cache Gradle packages
|
||||
uses: actions/cache@v2
|
||||
java-version: '17'
|
||||
- name: Unit Tests
|
||||
uses: burrunan/gradle-cache-action@v1
|
||||
with:
|
||||
path: ~/.gradle/caches
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle.kts') }}
|
||||
restore-keys: ${{ runner.os }}-gradle
|
||||
- name: Assemble with Gradle
|
||||
run: ./gradlew assemble
|
||||
- name: Run Unit Tests
|
||||
run: ./gradlew test
|
||||
gradle-version: wrapper
|
||||
arguments: test koverCollectReports
|
||||
|
15
.github/workflows/publish.yml
vendored
15
.github/workflows/publish.yml
vendored
@ -2,6 +2,8 @@ name: Publish to GitHub Packages
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
paths-ignore:
|
||||
- docs/**
|
||||
env:
|
||||
ORG_GRADLE_PROJECT_signingKey: ${{ secrets.SONATYPE_SIGNING_KEY }}
|
||||
ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.SONATYPE_SIGNING_PASSWORD }}
|
||||
@ -13,14 +15,11 @@ jobs:
|
||||
- uses: actions/setup-java@v2
|
||||
with:
|
||||
distribution: 'adopt'
|
||||
java-version: '11'
|
||||
- name: Cache Gradle packages
|
||||
uses: actions/cache@v2
|
||||
java-version: '17'
|
||||
- name: Publish to GitHub Packages
|
||||
uses: burrunan/gradle-cache-action@v1
|
||||
with:
|
||||
path: ~/.gradle/caches
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle.kts') }}
|
||||
restore-keys: ${{ runner.os }}-gradle
|
||||
- name: Publish package
|
||||
run: ./gradlew publishAllPublicationsToGithubPackagesRepository
|
||||
gradle-version: wrapper
|
||||
arguments: publishAllPublicationsToGithubPackagesRepository
|
||||
env:
|
||||
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:
|
||||
release:
|
||||
types:
|
||||
@ -8,24 +8,6 @@ env:
|
||||
ORG_GRADLE_PROJECT_signingKey: ${{ secrets.SONATYPE_SIGNING_KEY }}
|
||||
ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.SONATYPE_SIGNING_PASSWORD }}
|
||||
jobs:
|
||||
publish-to-github:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-java@v2
|
||||
with:
|
||||
distribution: 'adopt'
|
||||
java-version: '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:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@ -33,15 +15,39 @@ jobs:
|
||||
- uses: actions/setup-java@v2
|
||||
with:
|
||||
distribution: 'adopt'
|
||||
java-version: '11'
|
||||
- name: Cache Gradle packages
|
||||
uses: actions/cache@v2
|
||||
java-version: '17'
|
||||
- name: Publlish to Maven Central
|
||||
uses: burrunan/gradle-cache-action@v1
|
||||
with:
|
||||
path: ~/.gradle/caches
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle.kts') }}
|
||||
restore-keys: ${{ runner.os }}-gradle
|
||||
- name: Publish packages to Github
|
||||
run: ./gradlew publishToSonatype closeAndReleaseSonatypeStagingRepository -Prelease=true
|
||||
gradle-version: wrapper
|
||||
arguments: publishToSonatype closeAndReleaseSonatypeStagingRepository
|
||||
properties: |
|
||||
release=true
|
||||
env:
|
||||
ORG_GRADLE_PROJECT_sonatypeUsername: ${{ secrets.SONATYPE_USER }}
|
||||
ORG_GRADLE_PROJECT_sonatypePassword: ${{ secrets.SONATYPE_PASSWORD }}
|
||||
build-documentation:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- publish-to-nexus
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||
- uses: actions/setup-java@v2
|
||||
with:
|
||||
distribution: 'adopt'
|
||||
java-version: '17'
|
||||
- name: Build New Documentation
|
||||
uses: burrunan/gradle-cache-action@v1
|
||||
with:
|
||||
gradle-version: wrapper
|
||||
arguments: dokkaHtmlMultiModule
|
||||
properties: |
|
||||
release=true
|
||||
- name: Push New Documentation
|
||||
uses: EndBug/add-and-commit@v8
|
||||
with:
|
||||
default_author: github_actions
|
||||
branch: main
|
||||
message: 'doc: Added Latest Documentation ✨'
|
||||
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,7 +1,3 @@
|
||||
# Ignore Gradle project-specific cache directory
|
||||
.gradle
|
||||
|
||||
# Ignore Gradle build output directory
|
||||
build
|
||||
|
||||
.idea
|
||||
|
@ -1,3 +0,0 @@
|
||||
kotlin 1.5.0-M2
|
||||
java openjdk-14.0.1
|
||||
gradle 7.0
|
174
CHANGELOG.md
174
CHANGELOG.md
@ -1,5 +1,171 @@
|
||||
# Changelog
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Added
|
||||
|
||||
### Changed
|
||||
|
||||
### Remove
|
||||
|
||||
---
|
||||
|
||||
## Released
|
||||
|
||||
## [2.0.0] - January 23rd, 2022
|
||||
|
||||
Major Release 🎉 As we head towards the Ktor 2 release, this library will be kept compatible with Ktor 1. A future
|
||||
Kompendium 2 repository will be created soon, porting much of the changes you see here, with some awesome Ktor 2 twists
|
||||
😉
|
||||
|
||||
### Added
|
||||
|
||||
- Support for HTTP Patch, Head, and Options methods
|
||||
- Support for including parameter examples via `MethodInfo`
|
||||
- Dokka Pipeline Generation
|
||||
- GitHub Pages integration
|
||||
- Sourdough Gradle updates
|
||||
- Support for OAuth authentication
|
||||
- Gradle Toolchain feature to ensure match between local JDK and compile target
|
||||
- Dokka integration
|
||||
- Post-processing callback hook
|
||||
- `description` key to KompendiumField
|
||||
- Set of base constraints for simple and formatted types
|
||||
- Ability to document expected unstructured data
|
||||
|
||||
### Changed
|
||||
|
||||
- Kompendium now leverages the chosen API serializer. Supports Jackson, Gson and Kotlinx Serialization
|
||||
- Fixed bug where overridden field names were not reflected in serialized object and required array
|
||||
- Fixed bug where Ktor Location parents were not being scanned for parameters
|
||||
- `$ref` types are no longer generated, instead all objects are defined explicitly
|
||||
- All OpenAPI domain models moved to a separate module `kompendium-oas`
|
||||
- Moved all files in `kompendium-core` into `io.bkbn.kompendium.core` package from `io.bkbn.kompendium`
|
||||
- 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
|
||||
|
||||
This is just to get my repo back to normal now that I have confirmed sonatype publish is happening
|
||||
@ -112,7 +278,8 @@ This is just to get my repo back to normal now that I have confirmed sonatype pu
|
||||
|
||||
### Added
|
||||
|
||||
- Added an explicit `PathCalculator` interface to allow for easier handling of routes external to the core set of Ktor route selectors.
|
||||
- Added an explicit `PathCalculator` interface to allow for easier handling of routes external to the core set of Ktor
|
||||
route selectors.
|
||||
|
||||
## [0.5.1] - April 19th, 2021
|
||||
|
||||
@ -203,12 +370,13 @@ This is just to get my repo back to normal now that I have confirmed sonatype pu
|
||||
|
||||
### Added
|
||||
|
||||
- Beginning of an implementation. Currently, able to generate a rough outline of the API at runtime, along with generating
|
||||
full data classes represented by JSON Schema.
|
||||
- Beginning of an implementation. Currently, able to generate a rough outline of the API at runtime, along with
|
||||
generating full data classes represented by JSON Schema.
|
||||
|
||||
## [0.0.1] - April 11th, 2021
|
||||
|
||||
### Added
|
||||
|
||||
- Added _most_ of the data classes necessary for generating an [Open API Spec](https://swagger.io/specification)
|
||||
- Added playground to allow users to tinker with a live Ktor api in conjunction with development
|
||||
- Added all standard OSS files
|
||||
|
41
Project.md
Normal file
41
Project.md
Normal file
@ -0,0 +1,41 @@
|
||||
# Kompendium
|
||||
|
||||
Welcome to Kompendium, the straight-forward, minimally-invasive OpenAPI generator for Ktor.
|
||||
|
||||
## How to install
|
||||
|
||||
Kompendium publishes all releases to Maven Central. As such, using the release versions of `Kompendium` is as simple as
|
||||
declaring it as an implementation dependency in your `build.gradle.kts`
|
||||
|
||||
```kotlin
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("io.bkbn:kompendium-core:latest.release")
|
||||
}
|
||||
```
|
||||
|
||||
In addition to publishing releases to Maven Central, a snapshot version gets published to GitHub Packages on every merge
|
||||
to `main`. These can be consumed by adding the repository to your gradle build file. Instructions can be
|
||||
found [here](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-gradle-registry#using-a-published-package)
|
||||
|
||||
## Setting up the Kompendium Plugin
|
||||
|
||||
Kompendium is instantiated as a Ktor Feature/Plugin. It can be added to your API as follows
|
||||
|
||||
```kotlin
|
||||
private fun Application.mainModule() {
|
||||
// Installs the Kompendium Plugin and sets up baseline server metadata
|
||||
install(Kompendium) {
|
||||
spec = OpenApiSpec(/*..*/)
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## Notarization
|
||||
|
||||
The concept of notarizing routes / exceptions / etc. is central to Kompendium. More details on _how_ to notarize your
|
||||
API can be found in the kompendium-core module.
|
222
README.md
222
README.md
@ -1,215 +1,53 @@
|
||||
# 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
|
||||
|
||||
Kompendium is intended to be a minimally invasive OpenApi Specification generator for [Ktor](https://ktor.io).
|
||||
Minimally invasive meaning that users will use only Ktor native functions when implementing their API, and will
|
||||
supplement with Kompendium code in order to generate the appropriate spec.
|
||||
Kompendium is intended to be a minimally invasive OpenApi Specification generator for Ktor. Minimally invasive meaning
|
||||
that users will use only Ktor native functions when implementing their API, and will supplement with Kompendium code in
|
||||
order to generate the appropriate spec.
|
||||
|
||||
## How to install
|
||||
|
||||
Kompendium uses GitHub packages as its repository. Installing with Gradle is pretty painless. In your `build.gradle.kts`
|
||||
add the following
|
||||
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
|
||||
// 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 {
|
||||
github("https://maven.pkg.github.com/bkbnio/kompendium")
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
// 3 Add the package like any normal dependency
|
||||
dependencies {
|
||||
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()
|
||||
implementation("io.bkbn:kompendium-core:latest.release")
|
||||
}
|
||||
```
|
||||
|
||||
### 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
|
||||
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)
|
||||
|
||||
```kotlin
|
||||
routing {
|
||||
openApi(oas)
|
||||
redoc(oas)
|
||||
}
|
||||
```
|
||||
# Library Details
|
||||
|
||||
## Limitations
|
||||
Forthcoming, more details on V2 will be published soon :)
|
||||
|
||||
### Kompendium as a singleton
|
||||
## Local Development
|
||||
|
||||
Currently, Kompendium exists as a Kotlin object. This comes with a couple perks, but a couple downsides. Primarily,
|
||||
it offers a seriously clean UX where the implementer doesn't need to worry about what instance to send data to. The main
|
||||
drawback, however, is that you are limited to a single API per classpath.
|
||||
Kompendium should run locally right out of the box, no configuration necessary (assuming you have JDK 1.8+ installed).
|
||||
New features can be built locally and published to your local maven repository with the `./gradlew publishToMavenLocal`
|
||||
command!
|
||||
|
||||
If this is a blocker, please open a GitHub issue, and we can start to think out solutions!
|
||||
## The Playground
|
||||
|
||||
## Future Work
|
||||
Work on V1 of Kompendium has come to a close. This, however, does not mean it has achieved complete
|
||||
parity with the OpenAPI feature spec, nor does it have all-of-the nice to have features that a truly next-gen API spec
|
||||
should have. There are several outstanding features that have been added to the
|
||||
[V2 Milestone](https://github.com/bkbnio/kompendium/milestone/2). Among others, this includes
|
||||
This repo contains a `playground` module that contains a number of working examples showcasing the capabilities of
|
||||
Kompendium.
|
||||
|
||||
- AsyncAPI Integration
|
||||
- Field Validation
|
||||
- MavenCentral Release
|
||||
|
||||
If you have a feature that you would like to see implemented that is not on this list, or discover a 🐞, please open
|
||||
an issue [here](https://github.com/bkbnio/kompendium/issues/new)
|
||||
Feel free to check it out, or even create your own example!
|
||||
|
@ -1,13 +1,22 @@
|
||||
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 {
|
||||
id("org.jetbrains.kotlin.jvm") version "1.5.0" apply false
|
||||
id("io.gitlab.arturbosch.detekt") version "1.17.0-RC3" apply false
|
||||
id("com.adarshr.test-logger") version "3.0.0" apply false
|
||||
id("io.github.gradle-nexus.publish-plugin") version "1.1.0" apply true
|
||||
kotlin("jvm") version "1.6.10" apply false
|
||||
kotlin("plugin.serialization") version "1.6.10" apply false
|
||||
id("io.bkbn.sourdough.library.jvm") version "0.6.0" apply false
|
||||
id("io.bkbn.sourdough.application.jvm") version "0.6.0" apply false
|
||||
id("io.bkbn.sourdough.root") version "0.6.0"
|
||||
id("com.github.jakemarsden.git-hooks") version "0.0.2"
|
||||
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 {
|
||||
@ -20,59 +29,18 @@ allprojects {
|
||||
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/"))
|
||||
subprojects {
|
||||
plugins.withType(io.bkbn.sourdough.gradle.library.jvm.LibraryJvmPlugin::class) {
|
||||
extensions.configure(io.bkbn.sourdough.gradle.library.jvm.LibraryJvmExtension::class) {
|
||||
githubOrg.set("bkbnio")
|
||||
githubRepo.set("kompendium")
|
||||
licenseName.set("MIT License")
|
||||
licenseUrl.set("https://mit-license.org")
|
||||
developerId.set("unredundant")
|
||||
developerName.set("Ryan Brink")
|
||||
developerEmail.set("admin@bkbn.io")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
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:
|
||||
active: true
|
||||
ComplexCondition:
|
||||
active: true
|
||||
threshold: 4
|
||||
ComplexInterface:
|
||||
TooManyFunctions:
|
||||
active: false
|
||||
threshold: 10
|
||||
includeStaticDeclarations: false
|
||||
includePrivateDeclarations: false
|
||||
ComplexMethod:
|
||||
active: true
|
||||
threshold: 25
|
||||
ignoreSingleWhenExpression: false
|
||||
ignoreSimpleWhenEntries: false
|
||||
ignoreNestingFunctions: false
|
||||
nestingFunctions: [run, let, apply, with, also, use, forEach, isNotNull, ifNull]
|
||||
LabeledExpression:
|
||||
active: false
|
||||
ignoredLabels: []
|
||||
LargeClass:
|
||||
active: true
|
||||
threshold: 600
|
||||
LongMethod:
|
||||
active: true
|
||||
threshold: 80
|
||||
LongParameterList:
|
||||
active: true
|
||||
functionThreshold: 10
|
||||
constructorThreshold: 10
|
||||
ignoreDefaultParameters: false
|
||||
ignoreDataClasses: true
|
||||
ignoreAnnotated: []
|
||||
MethodOverloading:
|
||||
active: false
|
||||
threshold: 6
|
||||
NamedArguments:
|
||||
active: false
|
||||
threshold: 3
|
||||
NestedBlockDepth:
|
||||
active: true
|
||||
threshold: 6
|
||||
ReplaceSafeCallChainWithRun:
|
||||
active: false
|
||||
StringLiteralDuplication:
|
||||
active: false
|
||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
threshold: 3
|
||||
ignoreAnnotation: true
|
||||
excludeStringsWithLessThan5Characters: true
|
||||
ignoreStringsRegex: '$^'
|
||||
TooManyFunctions:
|
||||
active: false
|
||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
thresholdInFiles: 11
|
||||
thresholdInClasses: 11
|
||||
thresholdInInterfaces: 11
|
||||
thresholdInObjects: 11
|
||||
thresholdInEnums: 11
|
||||
ignoreDeprecated: false
|
||||
ignorePrivate: false
|
||||
ignoreOverridden: false
|
||||
|
||||
coroutines:
|
||||
active: true
|
||||
GlobalCoroutineUsage:
|
||||
active: false
|
||||
RedundantSuspendModifier:
|
||||
active: false
|
||||
SleepInsteadOfDelay:
|
||||
active: false
|
||||
SuspendFunWithFlowReturnType:
|
||||
active: false
|
||||
|
||||
empty-blocks:
|
||||
active: true
|
||||
EmptyCatchBlock:
|
||||
active: true
|
||||
allowedExceptionNameRegex: '_|(ignore|expected).*'
|
||||
EmptyClassBlock:
|
||||
active: true
|
||||
EmptyDefaultConstructor:
|
||||
active: true
|
||||
EmptyDoWhileBlock:
|
||||
active: true
|
||||
EmptyElseBlock:
|
||||
active: true
|
||||
EmptyFinallyBlock:
|
||||
active: true
|
||||
EmptyForBlock:
|
||||
active: true
|
||||
EmptyFunctionBlock:
|
||||
active: true
|
||||
ignoreOverridden: false
|
||||
EmptyIfBlock:
|
||||
active: true
|
||||
EmptyInitBlock:
|
||||
active: true
|
||||
EmptyKtFile:
|
||||
active: true
|
||||
EmptySecondaryConstructor:
|
||||
active: true
|
||||
EmptyTryBlock:
|
||||
active: true
|
||||
EmptyWhenBlock:
|
||||
active: true
|
||||
EmptyWhileBlock:
|
||||
active: true
|
||||
|
||||
exceptions:
|
||||
active: true
|
||||
ExceptionRaisedInUnexpectedLocation:
|
||||
active: true
|
||||
methodNames: [toString, hashCode, equals, finalize]
|
||||
InstanceOfCheckForException:
|
||||
active: false
|
||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
NotImplementedDeclaration:
|
||||
active: false
|
||||
ObjectExtendsThrowable:
|
||||
active: false
|
||||
PrintStackTrace:
|
||||
active: true
|
||||
RethrowCaughtException:
|
||||
active: true
|
||||
ReturnFromFinally:
|
||||
active: true
|
||||
ignoreLabeled: false
|
||||
SwallowedException:
|
||||
active: true
|
||||
ignoredExceptionTypes:
|
||||
- InterruptedException
|
||||
- NumberFormatException
|
||||
- ParseException
|
||||
- MalformedURLException
|
||||
allowedExceptionNameRegex: '_|(ignore|expected).*'
|
||||
ThrowingExceptionFromFinally:
|
||||
active: true
|
||||
ThrowingExceptionInMain:
|
||||
active: false
|
||||
ThrowingExceptionsWithoutMessageOrCause:
|
||||
active: true
|
||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
exceptions:
|
||||
- IllegalArgumentException
|
||||
- IllegalStateException
|
||||
- IOException
|
||||
ThrowingNewInstanceOfSameException:
|
||||
active: true
|
||||
TooGenericExceptionCaught:
|
||||
active: true
|
||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
exceptionNames:
|
||||
- ArrayIndexOutOfBoundsException
|
||||
- Error
|
||||
- Exception
|
||||
- IllegalMonitorStateException
|
||||
- NullPointerException
|
||||
- IndexOutOfBoundsException
|
||||
- RuntimeException
|
||||
- Throwable
|
||||
allowedExceptionNameRegex: '_|(ignore|expected).*'
|
||||
TooGenericExceptionThrown:
|
||||
active: true
|
||||
exceptionNames:
|
||||
- Error
|
||||
- Exception
|
||||
- Throwable
|
||||
- RuntimeException
|
||||
|
||||
ComplexMethod:
|
||||
threshold: 20
|
||||
formatting:
|
||||
active: true
|
||||
android: false
|
||||
autoCorrect: true
|
||||
AnnotationOnSeparateLine:
|
||||
active: false
|
||||
autoCorrect: true
|
||||
AnnotationSpacing:
|
||||
active: false
|
||||
autoCorrect: true
|
||||
ArgumentListWrapping:
|
||||
active: false
|
||||
autoCorrect: true
|
||||
ChainWrapping:
|
||||
active: true
|
||||
autoCorrect: true
|
||||
CommentSpacing:
|
||||
active: true
|
||||
autoCorrect: true
|
||||
EnumEntryNameCase:
|
||||
active: false
|
||||
autoCorrect: true
|
||||
Filename:
|
||||
active: true
|
||||
FinalNewline:
|
||||
active: true
|
||||
autoCorrect: true
|
||||
insertFinalNewLine: true
|
||||
ImportOrdering:
|
||||
active: false
|
||||
autoCorrect: true
|
||||
layout: 'idea'
|
||||
Indentation:
|
||||
active: false
|
||||
autoCorrect: true
|
||||
indentSize: 4
|
||||
continuationIndentSize: 4
|
||||
MaximumLineLength:
|
||||
active: true
|
||||
maxLineLength: 120
|
||||
ModifierOrdering:
|
||||
active: true
|
||||
autoCorrect: true
|
||||
MultiLineIfElse:
|
||||
active: true
|
||||
autoCorrect: true
|
||||
NoBlankLineBeforeRbrace:
|
||||
active: true
|
||||
autoCorrect: true
|
||||
NoConsecutiveBlankLines:
|
||||
active: true
|
||||
autoCorrect: true
|
||||
NoEmptyClassBody:
|
||||
active: true
|
||||
autoCorrect: true
|
||||
NoEmptyFirstLineInMethodBlock:
|
||||
active: false
|
||||
autoCorrect: true
|
||||
NoLineBreakAfterElse:
|
||||
active: true
|
||||
autoCorrect: true
|
||||
NoLineBreakBeforeAssignment:
|
||||
active: true
|
||||
autoCorrect: true
|
||||
NoMultipleSpaces:
|
||||
active: true
|
||||
autoCorrect: true
|
||||
NoSemicolons:
|
||||
active: true
|
||||
autoCorrect: true
|
||||
NoTrailingSpaces:
|
||||
active: true
|
||||
autoCorrect: true
|
||||
NoUnitReturn:
|
||||
active: true
|
||||
autoCorrect: true
|
||||
NoUnusedImports:
|
||||
active: true
|
||||
autoCorrect: true
|
||||
NoWildcardImports:
|
||||
active: true
|
||||
PackageName:
|
||||
active: true
|
||||
autoCorrect: true
|
||||
ParameterListWrapping:
|
||||
active: false
|
||||
autoCorrect: true
|
||||
indentSize: 4
|
||||
SpacingAroundAngleBrackets:
|
||||
active: false
|
||||
autoCorrect: true
|
||||
SpacingAroundColon:
|
||||
active: true
|
||||
autoCorrect: true
|
||||
SpacingAroundComma:
|
||||
active: true
|
||||
autoCorrect: true
|
||||
SpacingAroundCurly:
|
||||
active: true
|
||||
autoCorrect: true
|
||||
SpacingAroundDot:
|
||||
active: true
|
||||
autoCorrect: true
|
||||
SpacingAroundDoubleColon:
|
||||
active: false
|
||||
autoCorrect: true
|
||||
SpacingAroundKeyword:
|
||||
active: true
|
||||
autoCorrect: true
|
||||
SpacingAroundOperators:
|
||||
active: true
|
||||
autoCorrect: true
|
||||
SpacingAroundParens:
|
||||
active: true
|
||||
autoCorrect: true
|
||||
SpacingAroundRangeOperator:
|
||||
active: true
|
||||
autoCorrect: true
|
||||
SpacingAroundUnaryOperator:
|
||||
active: false
|
||||
autoCorrect: true
|
||||
SpacingBetweenDeclarationsWithAnnotations:
|
||||
active: false
|
||||
autoCorrect: true
|
||||
SpacingBetweenDeclarationsWithComments:
|
||||
active: false
|
||||
autoCorrect: true
|
||||
StringTemplate:
|
||||
active: true
|
||||
autoCorrect: true
|
||||
|
||||
naming:
|
||||
active: true
|
||||
ClassNaming:
|
||||
active: true
|
||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
classPattern: '[A-Z][a-zA-Z0-9]*'
|
||||
ConstructorParameterNaming:
|
||||
active: false
|
||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
parameterPattern: '[a-z][A-Za-z0-9]*'
|
||||
privateParameterPattern: '[a-z][A-Za-z0-9]*'
|
||||
excludeClassPattern: '$^'
|
||||
ignoreOverridden: true
|
||||
EnumNaming:
|
||||
active: true
|
||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
enumEntryPattern: '[A-Z][_a-zA-Z0-9]*'
|
||||
ForbiddenClassName:
|
||||
active: false
|
||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
forbiddenName: []
|
||||
FunctionMaxLength:
|
||||
active: false
|
||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
maximumFunctionNameLength: 30
|
||||
FunctionMinLength:
|
||||
active: false
|
||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
minimumFunctionNameLength: 3
|
||||
FunctionNaming:
|
||||
active: true
|
||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
functionPattern: '([a-z][a-zA-Z0-9]*)|(`.*`)'
|
||||
excludeClassPattern: '$^'
|
||||
ignoreOverridden: true
|
||||
ignoreAnnotated: ['Composable']
|
||||
FunctionParameterNaming:
|
||||
active: true
|
||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
parameterPattern: '[a-z][A-Za-z0-9]*'
|
||||
excludeClassPattern: '$^'
|
||||
ignoreOverridden: true
|
||||
InvalidPackageDeclaration:
|
||||
active: false
|
||||
excludes: ['*.kts']
|
||||
rootPackage: ''
|
||||
MatchingDeclarationName:
|
||||
active: true
|
||||
mustBeFirst: true
|
||||
MemberNameEqualsClassName:
|
||||
active: true
|
||||
ignoreOverridden: true
|
||||
NonBooleanPropertyPrefixedWithIs:
|
||||
active: false
|
||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
ObjectPropertyNaming:
|
||||
active: true
|
||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
constantPattern: '[A-Za-z][_A-Za-z0-9]*'
|
||||
propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
|
||||
privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*'
|
||||
PackageNaming:
|
||||
active: true
|
||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*'
|
||||
TopLevelPropertyNaming:
|
||||
active: true
|
||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
constantPattern: '[A-Z][_A-Z0-9]*'
|
||||
propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
|
||||
privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*'
|
||||
VariableMaxLength:
|
||||
active: false
|
||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
maximumVariableNameLength: 64
|
||||
VariableMinLength:
|
||||
active: false
|
||||
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
minimumVariableNameLength: 1
|
||||
VariableNaming:
|
||||
active: true
|
||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
variablePattern: '[a-z][A-Za-z0-9]*'
|
||||
privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*'
|
||||
excludeClassPattern: '$^'
|
||||
ignoreOverridden: true
|
||||
|
||||
performance:
|
||||
active: true
|
||||
ArrayPrimitive:
|
||||
active: true
|
||||
ForEachOnRange:
|
||||
active: true
|
||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
SpreadOperator:
|
||||
active: true
|
||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
UnnecessaryTemporaryInstantiation:
|
||||
active: true
|
||||
|
||||
potential-bugs:
|
||||
active: true
|
||||
Deprecation:
|
||||
active: false
|
||||
DontDowncastCollectionTypes:
|
||||
active: false
|
||||
DuplicateCaseInWhenExpression:
|
||||
active: true
|
||||
EqualsAlwaysReturnsTrueOrFalse:
|
||||
active: true
|
||||
EqualsWithHashCodeExist:
|
||||
active: true
|
||||
ExitOutsideMain:
|
||||
active: false
|
||||
ExplicitGarbageCollectionCall:
|
||||
active: true
|
||||
HasPlatformType:
|
||||
active: false
|
||||
IgnoredReturnValue:
|
||||
active: false
|
||||
restrictToAnnotatedMethods: true
|
||||
returnValueAnnotations: ['*.CheckReturnValue', '*.CheckResult']
|
||||
ImplicitDefaultLocale:
|
||||
active: true
|
||||
ImplicitUnitReturnType:
|
||||
active: false
|
||||
allowExplicitReturnType: true
|
||||
InvalidRange:
|
||||
active: true
|
||||
IteratorHasNextCallsNextMethod:
|
||||
active: true
|
||||
IteratorNotThrowingNoSuchElementException:
|
||||
active: true
|
||||
LateinitUsage:
|
||||
active: false
|
||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
excludeAnnotatedProperties: []
|
||||
ignoreOnClassesPattern: ''
|
||||
MapGetWithNotNullAssertionOperator:
|
||||
active: false
|
||||
MissingWhenCase:
|
||||
active: true
|
||||
allowElseExpression: true
|
||||
NullableToStringCall:
|
||||
active: false
|
||||
RedundantElseInWhen:
|
||||
active: true
|
||||
UnconditionalJumpStatementInLoop:
|
||||
active: false
|
||||
UnnecessaryNotNullOperator:
|
||||
active: true
|
||||
UnnecessarySafeCall:
|
||||
active: true
|
||||
UnreachableCatchBlock:
|
||||
active: false
|
||||
UnreachableCode:
|
||||
active: true
|
||||
UnsafeCallOnNullableType:
|
||||
active: true
|
||||
UnsafeCast:
|
||||
active: true
|
||||
UselessPostfixExpression:
|
||||
active: false
|
||||
WrongEqualsTypeParameter:
|
||||
active: true
|
||||
|
||||
style:
|
||||
active: true
|
||||
ClassOrdering:
|
||||
active: false
|
||||
CollapsibleIfStatements:
|
||||
active: false
|
||||
DataClassContainsFunctions:
|
||||
active: false
|
||||
conversionFunctionPrefix: 'to'
|
||||
DataClassShouldBeImmutable:
|
||||
active: false
|
||||
DestructuringDeclarationWithTooManyEntries:
|
||||
active: false
|
||||
maxDestructuringEntries: 3
|
||||
EqualsNullCall:
|
||||
active: true
|
||||
EqualsOnSignatureLine:
|
||||
active: false
|
||||
ExplicitCollectionElementAccessMethod:
|
||||
active: false
|
||||
ExplicitItLambdaParameter:
|
||||
active: false
|
||||
ExpressionBodySyntax:
|
||||
active: false
|
||||
includeLineWrapping: false
|
||||
ForbiddenComment:
|
||||
active: true
|
||||
values: ['TODO:', 'FIXME:', 'STOPSHIP:']
|
||||
allowedPatterns: ''
|
||||
ForbiddenImport:
|
||||
active: false
|
||||
imports: []
|
||||
forbiddenPatterns: ''
|
||||
ForbiddenMethodCall:
|
||||
active: false
|
||||
methods: ['kotlin.io.println', 'kotlin.io.print']
|
||||
ForbiddenPublicDataClass:
|
||||
active: true
|
||||
excludes: ['**']
|
||||
ignorePackages: ['*.internal', '*.internal.*']
|
||||
ForbiddenVoid:
|
||||
active: false
|
||||
ignoreOverridden: false
|
||||
ignoreUsageInGenerics: false
|
||||
FunctionOnlyReturningConstant:
|
||||
active: true
|
||||
ignoreOverridableFunction: true
|
||||
ignoreActualFunction: true
|
||||
excludedFunctions: 'describeContents'
|
||||
excludeAnnotatedFunction: ['dagger.Provides']
|
||||
LibraryCodeMustSpecifyReturnType:
|
||||
active: true
|
||||
excludes: ['**']
|
||||
LibraryEntitiesShouldNotBePublic:
|
||||
active: true
|
||||
excludes: ['**']
|
||||
LoopWithTooManyJumpStatements:
|
||||
active: true
|
||||
maxJumpCount: 1
|
||||
MagicNumber:
|
||||
active: true
|
||||
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
|
||||
ignoreNumbers: ['-1', '0', '1', '2']
|
||||
ignoreHashCodeFunction: true
|
||||
ignorePropertyDeclaration: false
|
||||
ignoreLocalVariableDeclaration: false
|
||||
ignoreConstantDeclaration: true
|
||||
ignoreCompanionObjectPropertyDeclaration: true
|
||||
ignoreAnnotation: false
|
||||
ignoreNamedArgument: true
|
||||
ignoreEnums: false
|
||||
ignoreRanges: false
|
||||
ignoreExtensionFunctions: true
|
||||
MandatoryBracesIfStatements:
|
||||
active: false
|
||||
MandatoryBracesLoops:
|
||||
active: false
|
||||
MaxLineLength:
|
||||
excludes: ['**/test/**/*']
|
||||
active: true
|
||||
maxLineLength: 120
|
||||
excludePackageStatements: true
|
||||
excludeImportStatements: true
|
||||
excludeCommentStatements: false
|
||||
MayBeConst:
|
||||
active: true
|
||||
ModifierOrder:
|
||||
active: true
|
||||
MultilineLambdaItParameter:
|
||||
MagicNumber:
|
||||
excludes: ['**/kompendium-playground/**/*', '**/test/**/*']
|
||||
naming:
|
||||
ConstructorParameterNaming:
|
||||
active: false
|
||||
NestedClassesVisibility:
|
||||
active: true
|
||||
NewLineAtEndOfFile:
|
||||
active: true
|
||||
NoTabs:
|
||||
performance:
|
||||
SpreadOperator:
|
||||
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
|
||||
project.version=1.2.2
|
||||
project.version=2.0.0
|
||||
# Kotlin
|
||||
kotlin.code.style=official
|
||||
# Gradle
|
||||
org.gradle.vfs.watch=true
|
||||
org.gradle.vfs.verbose=true
|
||||
org.gradle.jvmargs=-Xmx2000m
|
||||
|
||||
# Dependencies
|
||||
ktorVersion=1.6.7
|
||||
kotestVersion=5.1.0
|
||||
|
@ -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
|
||||
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
|
||||
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");
|
||||
# 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
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
|
||||
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.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
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 [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
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."
|
||||
fi
|
||||
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.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
@ -106,80 +140,95 @@ location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
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" ;;
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=`save "$@"`
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --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
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
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" "$@"
|
||||
|
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.
|
23
kompendium-annotations/build.gradle.kts
Normal file
23
kompendium-annotations/build.gradle.kts
Normal file
@ -0,0 +1,23 @@
|
||||
plugins {
|
||||
kotlin("jvm")
|
||||
id("io.bkbn.sourdough.library.jvm")
|
||||
id("io.gitlab.arturbosch.detekt")
|
||||
id("com.adarshr.test-logger")
|
||||
id("org.jetbrains.dokka")
|
||||
id("maven-publish")
|
||||
id("java-library")
|
||||
id("signing")
|
||||
}
|
||||
|
||||
sourdough {
|
||||
libraryName.set("Kompendium Annotations")
|
||||
libraryDescription.set("A set of annotations used by Kompendium to generate OpenAPI Specifications")
|
||||
}
|
||||
|
||||
testing {
|
||||
suites {
|
||||
named("test", JvmTestSuite::class) {
|
||||
useJUnitJupiter()
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
@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,37 @@
|
||||
plugins {
|
||||
`java-library`
|
||||
`maven-publish`
|
||||
signing
|
||||
kotlin("jvm")
|
||||
id("io.bkbn.sourdough.library.jvm")
|
||||
id("io.gitlab.arturbosch.detekt")
|
||||
id("com.adarshr.test-logger")
|
||||
id("org.jetbrains.dokka")
|
||||
id("maven-publish")
|
||||
id("java-library")
|
||||
id("signing")
|
||||
}
|
||||
|
||||
sourdough {
|
||||
libraryName.set("Kompendium Authentication")
|
||||
libraryDescription.set("Kompendium library to pair with Ktor Auth to provide authorization info to OpenAPI")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(platform("org.jetbrains.kotlin:kotlin-bom"))
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
|
||||
implementation(libs.bundles.ktor)
|
||||
implementation(libs.bundles.ktorAuth)
|
||||
// IMPLEMENTATION
|
||||
|
||||
val ktorVersion: String by project
|
||||
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")
|
||||
testImplementation("org.jetbrains.kotlin:kotlin-test-junit")
|
||||
testImplementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.12.0")
|
||||
testImplementation("io.ktor:ktor-server-test-host:1.5.3")
|
||||
// TESTING
|
||||
|
||||
testImplementation(testFixtures(projects.kompendiumCore))
|
||||
}
|
||||
|
||||
java {
|
||||
withSourcesJar()
|
||||
withJavadocJar()
|
||||
}
|
||||
|
||||
publishing {
|
||||
repositories {
|
||||
maven {
|
||||
name = "GithubPackages"
|
||||
url = uri("https://maven.pkg.github.com/bkbnio/kompendium")
|
||||
credentials {
|
||||
username = System.getenv("GITHUB_ACTOR")
|
||||
password = System.getenv("GITHUB_TOKEN")
|
||||
testing {
|
||||
suites {
|
||||
named("test", JvmTestSuite::class) {
|
||||
useJUnitJupiter()
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude
|
||||
import com.fasterxml.jackson.databind.SerializationFeature
|
||||
import io.ktor.application.Application
|
||||
import io.ktor.application.call
|
||||
import io.ktor.application.install
|
||||
import io.ktor.auth.Authentication
|
||||
import io.ktor.auth.UserIdPrincipal
|
||||
import io.ktor.auth.authenticate
|
||||
import io.ktor.features.ContentNegotiation
|
||||
import io.ktor.http.ContentType
|
||||
import io.ktor.http.HttpMethod
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.jackson.jackson
|
||||
import io.ktor.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
|
||||
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.util.AuthConfigName
|
||||
import io.bkbn.kompendium.auth.util.configBasicAuth
|
||||
import io.bkbn.kompendium.auth.util.configJwtAuth
|
||||
import io.bkbn.kompendium.auth.util.notarizedAuthRoute
|
||||
import io.bkbn.kompendium.auth.util.setupOauth
|
||||
import io.bkbn.kompendium.core.fixtures.TestHelpers.openApiTest
|
||||
import io.bkbn.kompendium.oas.security.OAuth
|
||||
import io.kotest.core.spec.style.DescribeSpec
|
||||
|
||||
internal class KompendiumAuthTest {
|
||||
|
||||
@AfterTest
|
||||
fun `reset kompendium`() {
|
||||
Kompendium.resetSchema()
|
||||
class KompendiumAuthTest : DescribeSpec({
|
||||
describe("Basic Authentication") {
|
||||
it("Can create a notarized basic authentication record with all expected information") {
|
||||
// arrange
|
||||
val authConfig = object : BasicAuthConfiguration {
|
||||
override val name: String = AuthConfigName.Basic
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Notarized Get with basic authentication records all expected information`() {
|
||||
withTestApplication({
|
||||
configModule()
|
||||
// act
|
||||
openApiTest("notarized_basic_authenticated_get.json") {
|
||||
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")
|
||||
notarizedAuthRoute(authConfig)
|
||||
}
|
||||
}
|
||||
}
|
||||
describe("JWT Authentication") {
|
||||
it("Can create a simple notarized JWT route") {
|
||||
// arrange
|
||||
val authConfig = object : JwtAuthConfiguration {
|
||||
override val name: String = AuthConfigName.JWT
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Notarized Get with jwt authentication records all expected information`() {
|
||||
withTestApplication({
|
||||
configModule()
|
||||
// act
|
||||
openApiTest("notarized_jwt_authenticated_get.json") {
|
||||
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
|
||||
val expected = TestData.getFileSnapshot("notarized_multiple_jwt_authenticated_get.json").trim()
|
||||
assertEquals(expected, json, "The received json spec should match the expected content")
|
||||
}
|
||||
}
|
||||
|
||||
private fun Application.configModule() {
|
||||
install(ContentNegotiation) {
|
||||
jackson(ContentType.Application.Json) {
|
||||
enable(SerializationFeature.INDENT_OUTPUT)
|
||||
setSerializationInclusion(JsonInclude.Include.NON_NULL)
|
||||
notarizedAuthRoute(authConfig)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Application.configBasicAuth() {
|
||||
install(Authentication) {
|
||||
notarizedBasic(TestData.AuthConfigName.Basic) {
|
||||
realm = "Ktor Server"
|
||||
validate { credentials ->
|
||||
if (credentials.name == credentials.password) {
|
||||
UserIdPrincipal(credentials.name)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Application.configJwtAuth(
|
||||
header: String? = null,
|
||||
scheme: String? = null
|
||||
) {
|
||||
install(Authentication) {
|
||||
notarizedJwt(TestData.AuthConfigName.JWT, header, scheme) {
|
||||
realm = "Ktor server"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
describe("OAuth Authentication") {
|
||||
it("Can create an Oauth schema with all possible flows") {
|
||||
// arrange
|
||||
val flows = OAuth.Flows(
|
||||
implicit = OAuth.Flows.Implicit(
|
||||
"https://accounts.google.com/o/oauth2/auth",
|
||||
scopes = mapOf("test" to "is a cool scope", "this" to "is also cool")
|
||||
),
|
||||
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")
|
||||
)
|
||||
|
||||
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,14 +1,38 @@
|
||||
{
|
||||
"openapi": "3.0.3",
|
||||
"info" : { },
|
||||
"servers" : [ ],
|
||||
"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" : [ {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "a",
|
||||
"in": "path",
|
||||
"schema": {
|
||||
@ -16,7 +40,8 @@
|
||||
},
|
||||
"required": true,
|
||||
"deprecated": false
|
||||
}, {
|
||||
},
|
||||
{
|
||||
"name": "aa",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
@ -25,44 +50,38 @@
|
||||
},
|
||||
"required": true,
|
||||
"deprecated": false
|
||||
} ],
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "A Successful Endeavor",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref" : "#/components/schemas/TestResponse"
|
||||
"properties": {
|
||||
"c": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"c"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"deprecated": false,
|
||||
"security" : [ {
|
||||
"security": [
|
||||
{
|
||||
"basic": []
|
||||
} ]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas" : {
|
||||
"String" : {
|
||||
"type" : "string"
|
||||
},
|
||||
"TestResponse" : {
|
||||
"properties" : {
|
||||
"c" : {
|
||||
"$ref" : "#/components/schemas/String"
|
||||
}
|
||||
},
|
||||
"type" : "object"
|
||||
},
|
||||
"Int" : {
|
||||
"format" : "int32",
|
||||
"type" : "integer"
|
||||
}
|
||||
},
|
||||
"securitySchemes": {
|
||||
"basic": {
|
||||
"type": "http",
|
||||
|
@ -1,14 +1,38 @@
|
||||
{
|
||||
"openapi": "3.0.3",
|
||||
"info" : { },
|
||||
"servers" : [ ],
|
||||
"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" : [ {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "a",
|
||||
"in": "path",
|
||||
"schema": {
|
||||
@ -16,7 +40,8 @@
|
||||
},
|
||||
"required": true,
|
||||
"deprecated": false
|
||||
}, {
|
||||
},
|
||||
{
|
||||
"name": "aa",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
@ -25,46 +50,41 @@
|
||||
},
|
||||
"required": true,
|
||||
"deprecated": false
|
||||
} ],
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "A Successful Endeavor",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref" : "#/components/schemas/TestResponse"
|
||||
"properties": {
|
||||
"c": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"c"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"deprecated": false,
|
||||
"security" : [ {
|
||||
"security": [
|
||||
{
|
||||
"jwt": []
|
||||
} ]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas" : {
|
||||
"String" : {
|
||||
"type" : "string"
|
||||
},
|
||||
"TestResponse" : {
|
||||
"properties" : {
|
||||
"c" : {
|
||||
"$ref" : "#/components/schemas/String"
|
||||
}
|
||||
},
|
||||
"type" : "object"
|
||||
},
|
||||
"Int" : {
|
||||
"format" : "int32",
|
||||
"type" : "integer"
|
||||
}
|
||||
},
|
||||
"securitySchemes": {
|
||||
"jwt": {
|
||||
"bearerFormat": "JWT",
|
||||
"type": "http",
|
||||
"scheme": "bearer"
|
||||
}
|
||||
|
@ -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,54 @@
|
||||
plugins {
|
||||
`java-library`
|
||||
`maven-publish`
|
||||
signing
|
||||
kotlin("jvm")
|
||||
id("io.bkbn.sourdough.library.jvm")
|
||||
id("io.gitlab.arturbosch.detekt")
|
||||
id("com.adarshr.test-logger")
|
||||
id("org.jetbrains.dokka")
|
||||
id("maven-publish")
|
||||
id("java-library")
|
||||
id("signing")
|
||||
id("java-test-fixtures")
|
||||
}
|
||||
|
||||
sourdough {
|
||||
libraryName.set("Kompendium Core")
|
||||
libraryDescription.set("Core functionality for the Kompendium library")
|
||||
compilerArgs.set(listOf("-opt-in=kotlin.RequiresOptIn"))
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(platform("org.jetbrains.kotlin:kotlin-bom"))
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
|
||||
implementation(libs.bundles.ktor)
|
||||
testImplementation("org.jetbrains.kotlin:kotlin-test")
|
||||
testImplementation("org.jetbrains.kotlin:kotlin-test-junit")
|
||||
testImplementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.12.0")
|
||||
testImplementation("io.ktor:ktor-server-test-host:1.5.3")
|
||||
// VERSIONS
|
||||
val ktorVersion: String by project
|
||||
val kotestVersion: String by project
|
||||
|
||||
// IMPLEMENTATION
|
||||
|
||||
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 {
|
||||
withSourcesJar()
|
||||
withJavadocJar()
|
||||
}
|
||||
|
||||
publishing {
|
||||
repositories {
|
||||
maven {
|
||||
name = "GithubPackages"
|
||||
url = uri("https://maven.pkg.github.com/bkbnio/kompendium")
|
||||
credentials {
|
||||
username = System.getenv("GITHUB_ACTOR")
|
||||
password = System.getenv("GITHUB_TOKEN")
|
||||
testing {
|
||||
suites {
|
||||
named("test", JvmTestSuite::class) {
|
||||
useJUnitJupiter()
|
||||
}
|
||||
}
|
||||
}
|
||||
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>(
|
||||
val description: String,
|
@ -1,4 +1,4 @@
|
||||
package io.bkbn.kompendium.models.meta
|
||||
package io.bkbn.kompendium.core.metadata
|
||||
|
||||
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.html.respondHtml
|
||||
@ -13,20 +13,19 @@ import kotlinx.html.script
|
||||
import kotlinx.html.style
|
||||
import kotlinx.html.title
|
||||
import kotlinx.html.unsafe
|
||||
import io.bkbn.kompendium.models.oas.OpenApiSpec
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
fun Routing.redoc(oas: OpenApiSpec, specUrl: String = "/openapi.json") {
|
||||
fun Routing.redoc(pageTitle: String = "Docs", specUrl: String = "/openapi.json") {
|
||||
route("/docs") {
|
||||
get {
|
||||
call.respondHtml {
|
||||
head {
|
||||
title {
|
||||
+"${oas.info.title}"
|
||||
+"$pageTitle"
|
||||
}
|
||||
meta {
|
||||
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 kotlin.reflect.KClass
|
||||
import kotlin.reflect.KProperty
|
||||
@ -8,17 +7,18 @@ import kotlin.reflect.KType
|
||||
import kotlin.reflect.full.createType
|
||||
import kotlin.reflect.jvm.javaField
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.util.Locale
|
||||
|
||||
object Helpers {
|
||||
|
||||
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() }
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
fun <T> logged(functionName: String, entities: Map<String, Any>, block: () -> T): T {
|
||||
@ -70,4 +70,16 @@ object Helpers {
|
||||
.map { it.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