Compare commits

..

16 Commits

39 changed files with 1117 additions and 877 deletions

View File

@ -37,3 +37,8 @@ jobs:
run: ./gradlew assemble run: ./gradlew assemble
- name: Run Unit Tests - name: Run Unit Tests
run: ./gradlew test run: ./gradlew test
- name: Cache Coverage Results
uses: actions/cache@v2
with:
path: ./**/build/reports/jacoco
key: ${{ runner.os }}-unit-${{ env.GITHUB_SHA }}

View File

@ -24,3 +24,23 @@ jobs:
run: ./gradlew publishAllPublicationsToGithubPackagesRepository run: ./gradlew publishAllPublicationsToGithubPackagesRepository
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
code-coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-java@v2
with:
distribution: 'adopt'
java-version: '11'
- name: Cache Gradle packages
uses: actions/cache@v2
with:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle.kts') }}
restore-keys: ${{ runner.os }}-gradle
- name: Run Unit Tests
run: ./gradlew test
- name: Publish code coverage to Codacy
uses: codacy/codacy-coverage-reporter-action@v1
with:
project-token: ${{ secrets.CODACY_PROJECT_TOKEN }}

View File

@ -1,5 +1,77 @@
# Changelog # Changelog
## [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 ## [1.3.0] - June 4th, 2021
### Changed ### Changed

110
README.md
View File

@ -1,9 +1,13 @@
# Kompendium # Kompendium
[![Codacy Badge](https://app.codacy.com/project/badge/Grade/a9bfd6c77d22497b907b3221849a3ba9)](https://www.codacy.com/gh/bkbnio/kompendium/dashboard?utm_source=github.com&utm_medium=referral&utm_content=bkbnio/kompendium&utm_campaign=Badge_Grade)
[![Codacy Badge](https://app.codacy.com/project/badge/Coverage/a9bfd6c77d22497b907b3221849a3ba9)](https://www.codacy.com/gh/bkbnio/kompendium/dashboard?utm_source=github.com&utm_medium=referral&utm_content=bkbnio/kompendium&utm_campaign=Badge_Coverage)
[![version](https://img.shields.io/maven-central/v/io.bkbn/kompendium-core?style=flat-square)](https://search.maven.org/search?q=io.bkbn%20kompendium) [![version](https://img.shields.io/maven-central/v/io.bkbn/kompendium-core?style=flat-square)](https://search.maven.org/search?q=io.bkbn%20kompendium)
## What is Kompendium ## What is Kompendium
### ⚠️ For info on V2 please see [here](#V2)
Kompendium is intended to be a minimally invasive OpenApi Specification generator for [Ktor](https://ktor.io). Kompendium is intended to be a minimally invasive OpenApi Specification generator for [Ktor](https://ktor.io).
Minimally invasive meaning that users will use only Ktor native functions when implementing their API, and will 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. supplement with Kompendium code in order to generate the appropriate spec.
@ -19,10 +23,11 @@ repositories {
} }
dependencies { dependencies {
// other (less cool) dependencies implementation("io.bkbn:kompendium-core:1.8.1")
implementation("io.bkbn:kompendium-core:latest") implementation("io.bkbn:kompendium-auth:1.8.1")
implementation("io.bkbn:kompendium-auth:latest") implementation("io.bkbn:kompendium-swagger-ui:1.8.1")
implementation("io.bkbn:kompendium-swagger-ui:latest")
// Other dependencies...
} }
``` ```
@ -50,11 +55,15 @@ repositories {
// 3 Add the package like any normal dependency // 3 Add the package like any normal dependency
dependencies { dependencies {
implementation("io.bkbn:kompendium-core:1.0.0") implementation("io.bkbn:kompendium-core:1.8.1")
} }
``` ```
## Local Development
Kompendium should run locally right out of the box, no configuration necessary (assuming you have JDK 1.8+ installed). New features can be built locally and published to your local maven repository with the `./gradlew publishToMavenLocal` command!
## In depth ## In depth
### Notarized Routes ### Notarized Routes
@ -96,9 +105,21 @@ The intended purpose of `KompendiumField` is to offer field level overrides such
The purpose of `KompendiumParam` is to provide supplemental information needed to properly assign the type of parameter 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. (cookie, header, query, path) as well as other parameter-level metadata.
### Undeclared Field
There is also a final `UndeclaredField` annotation. This should be used only in an absolutely emergency. This annotation
will allow you to inject a _single_ undeclared field that will be included as part of the schema.
Due to limitations in using repeated annotations, this can only be used once per class
This is a complete hack, and is included for odd scenarios like kotlinx serialization polymorphic adapters that expect a
`type` field in order to perform their analysis.
Use this _only_ when **all** else fails
### Polymorphism ### Polymorphism
Out of the box, Kompendium has support for sealed classes. At runtime, it will build a mapping of all available sub-classes Speaking of polymorphism... out of the box, Kompendium has support for sealed classes and interfaces. At runtime, it will build a mapping of all available sub-classes
and build a spec that takes `anyOf` the implementations. This is currently a weak point of the entire library, and 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 🤠 suggestions on better implementations are welcome 🤠
@ -107,6 +128,56 @@ suggestions on better implementations are welcome 🤠
Under the hood, Kompendium uses Jackson to serialize the final api spec. However, this implementation detail Under the hood, Kompendium uses Jackson to serialize the final api spec. However, this implementation detail
does not leak to the actual API, meaning that users are free to choose the serialization library of their choice. does not leak to the actual API, meaning that users are free to choose the serialization library of their choice.
Added the possibility to add your own ObjectMapper for Jackson.
Added a default parameter with the following configuration:
```kotlin
ObjectMapper()
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
.enable(SerializationFeature.INDENT_OUTPUT)
```
If you want to change this default configuration and use your own ObjectMapper you only need to pass it as a second argument to the openApi module:
```kotlin
routing {
openApi(oas, objectMapper)
route("/potato/spud") {
notarizedGet(simpleGetInfo) {
call.respond(HttpStatusCode.OK)
}
}
}
```
### Route Handling
> ⚠️ Warning: Custom route handling is almost definitely an indication that either a new selector should be added to kompendium-core or that kompendium is in need of another module to handle a new ktor companion module. If you have encountered a route selector that is not already handled, please consider opening an [issue](https://github.com/bkbnio/kompendium/issues/new)
Kompendium does its best to handle all Ktor routes out of the gate. However, in keeping with the modular approach of
Ktor and Kompendium, this is not always possible.
Should you need to, custom route handlers can be registered via the
`Kompendium.addCustomRouteHandler` function.
The handler signature is as follows
```kotlin
fun <T : RouteSelector> addCustomRouteHandler(
selector: KClass<T>,
handler: PathCalculator.(Route, String) -> String
)
```
This function takes a selector, which _must_ be a KClass of the Ktor `RouteSelector` type. The handler is a function
that extends the Kompendium `PathCalculator`. This is necessary because it gives you access to `PathCalculator.calculate`,
which you are going to want in order to recursively calculate the remainder of the route :)
Its parameters are the `Route` itself, along with the "tail" of the Path (the path that has been calculated thus far).
Working examples `init` blocks of the `PathCalculator` and `KompendiumAuth` object.
## Examples ## Examples
The full source code can be found in the `kompendium-playground` module. Here is a simple get endpoint example The full source code can be found in the `kompendium-playground` module. Here is a simple get endpoint example
@ -139,6 +210,7 @@ fun Application.mainModule() {
val simpleGetInfo = GetInfo<Unit, ExampleResponse>( val simpleGetInfo = GetInfo<Unit, ExampleResponse>(
summary = "Example Parameters", summary = "Example Parameters",
description = "A test for setting parameter examples", description = "A test for setting parameter examples",
operationId = "getExamples",
responseInfo = ResponseInfo( responseInfo = ResponseInfo(
status = 200, status = 200,
description = "nice", description = "nice",
@ -216,6 +288,25 @@ routing {
} }
``` ```
## Custom Type Overrides
Kompendium does its best to analyze types and to generate an OpenAPI format accordingly. However, there are certain
classes that just don't play nice with the standard reflection analysis that Kompendium performs.
Should you encounter a data type that Kompendium cannot comprehend, you will need to
add it explicitly. For example, adding the Joda Time `DateTime` object would be as simple as the following
```kotlin
Kompendium.addCustomTypeSchema(DateTime::class, FormatSchema("date-time", "string"))
```
Since `Kompendium` is an object, this needs to be declared once, ahead of the actual API instantiation. This way, this
type override can be cached ahead of reflection. Kompendium will then match all instances of this type and return the
specified schema.
So how do you know a type can and cannot be inferred? The safe bet is that it can be. So go ahead and give it a shot.
However, in the very odd scenario (almost always having to do with date/time libraries 😤) where it can't, you can rest
safely knowing that you have the option to inject a custom override should you need to.
## Limitations ## Limitations
### Kompendium as a singleton ### Kompendium as a singleton
@ -234,7 +325,12 @@ should have. There are several outstanding features that have been added to the
- AsyncAPI Integration - AsyncAPI Integration
- Field Validation - 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 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) an issue [here](https://github.com/bkbnio/kompendium/issues/new)
### V2
Due to the large number of breaking changes that will be made in version 2, development is currently being done on the
long-lived `v2` feature branch. If you are working on any feature in the `V2` milestone, please target that branch!
If you are unsure where your changes should be, please open an issue first :)

View File

@ -4,8 +4,8 @@ import io.gitlab.arturbosch.detekt.extensions.DetektExtension
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins { plugins {
id("org.jetbrains.kotlin.jvm") version "1.5.0" apply false id("org.jetbrains.kotlin.jvm") version "1.5.31" apply false
id("io.gitlab.arturbosch.detekt") version "1.17.0-RC3" apply false id("io.gitlab.arturbosch.detekt") version "1.18.1" apply false
id("com.adarshr.test-logger") version "3.0.0" apply false id("com.adarshr.test-logger") version "3.0.0" apply false
id("io.github.gradle-nexus.publish-plugin") version "1.1.0" apply true id("io.github.gradle-nexus.publish-plugin") version "1.1.0" apply true
} }
@ -30,13 +30,30 @@ allprojects {
apply(plugin = "io.gitlab.arturbosch.detekt") apply(plugin = "io.gitlab.arturbosch.detekt")
apply(plugin = "com.adarshr.test-logger") apply(plugin = "com.adarshr.test-logger")
apply(plugin = "idea") apply(plugin = "idea")
apply(plugin = "jacoco")
tasks.withType<KotlinCompile>().configureEach { tasks.withType<KotlinCompile>().configureEach {
kotlinOptions { kotlinOptions {
jvmTarget = "11" jvmTarget = "1.8"
} }
} }
tasks.withType<Test>() {
finalizedBy(tasks.withType(JacocoReport::class))
}
tasks.withType<JacocoReport>() {
reports {
html.required.set(true)
xml.required.set(true)
}
}
configure<JacocoPluginExtension> {
toolVersion = "0.8.7"
}
@Suppress("MagicNumber")
configure<TestLoggerExtension> { configure<TestLoggerExtension> {
theme = ThemeType.MOCHA theme = ThemeType.MOCHA
setLogLevel("lifecycle") setLogLevel("lifecycle")
@ -57,7 +74,7 @@ allprojects {
} }
configure<DetektExtension> { configure<DetektExtension> {
toolVersion = "1.17.0-RC3" toolVersion = "1.18.1"
config = files("${rootProject.projectDir}/detekt.yml") config = files("${rootProject.projectDir}/detekt.yml")
buildUponDefaultConfig = true buildUponDefaultConfig = true
} }

View File

@ -1,710 +1,20 @@
build:
maxIssues: 0
excludeCorrectable: false
weights:
# complexity: 2
# LongParameterList: 1
# style: 1
# comments: 1
config:
validation: true
warningsAsErrors: false
# when writing own rules with new properties, exclude the property path e.g.: 'my_rule_set,.*>.*>[my_property]'
excludes: ''
processors:
active: true
exclude:
- 'DetektProgressListener'
console-reports:
active: true
exclude:
- 'ProjectStatisticsReport'
- 'ComplexityReport'
- 'NotificationReport'
- 'FileBasedFindingsReport'
output-reports:
active: true
comments:
active: true
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
AbsentOrWrongFileLicense:
active: false
licenseTemplateFile: 'license.template'
CommentOverPrivateFunction:
active: false
CommentOverPrivateProperty:
active: false
EndOfSentenceFormat:
active: false
endOfSentenceFormat: '([.?!][ \t\n\r\f<])|([.?!:]$)'
UndocumentedPublicClass:
active: false
searchInNestedClass: true
searchInInnerClass: true
searchInInnerObject: true
searchInInnerInterface: true
UndocumentedPublicFunction:
active: false
UndocumentedPublicProperty:
active: false
complexity: complexity:
active: true TooManyFunctions:
ComplexCondition:
active: true
threshold: 4
ComplexInterface:
active: false active: false
threshold: 10
includeStaticDeclarations: false
includePrivateDeclarations: false
ComplexMethod:
active: true
threshold: 25
ignoreSingleWhenExpression: false
ignoreSimpleWhenEntries: false
ignoreNestingFunctions: false
nestingFunctions: [run, let, apply, with, also, use, forEach, isNotNull, ifNull]
LabeledExpression:
active: false
ignoredLabels: []
LargeClass:
active: true
threshold: 600
LongMethod:
active: true
threshold: 80
LongParameterList: LongParameterList:
active: true active: true
functionThreshold: 10 functionThreshold: 10
constructorThreshold: 10 constructorThreshold: 10
ignoreDefaultParameters: false ComplexMethod:
ignoreDataClasses: true threshold: 20
ignoreAnnotated: []
MethodOverloading:
active: false
threshold: 6
NamedArguments:
active: false
threshold: 3
NestedBlockDepth:
active: true
threshold: 6
ReplaceSafeCallChainWithRun:
active: false
StringLiteralDuplication:
active: false
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
threshold: 3
ignoreAnnotation: true
excludeStringsWithLessThan5Characters: true
ignoreStringsRegex: '$^'
TooManyFunctions:
active: false
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
thresholdInFiles: 11
thresholdInClasses: 11
thresholdInInterfaces: 11
thresholdInObjects: 11
thresholdInEnums: 11
ignoreDeprecated: false
ignorePrivate: false
ignoreOverridden: false
coroutines:
active: true
GlobalCoroutineUsage:
active: false
RedundantSuspendModifier:
active: false
SleepInsteadOfDelay:
active: false
SuspendFunWithFlowReturnType:
active: false
empty-blocks:
active: true
EmptyCatchBlock:
active: true
allowedExceptionNameRegex: '_|(ignore|expected).*'
EmptyClassBlock:
active: true
EmptyDefaultConstructor:
active: true
EmptyDoWhileBlock:
active: true
EmptyElseBlock:
active: true
EmptyFinallyBlock:
active: true
EmptyForBlock:
active: true
EmptyFunctionBlock:
active: true
ignoreOverridden: false
EmptyIfBlock:
active: true
EmptyInitBlock:
active: true
EmptyKtFile:
active: true
EmptySecondaryConstructor:
active: true
EmptyTryBlock:
active: true
EmptyWhenBlock:
active: true
EmptyWhileBlock:
active: true
exceptions:
active: true
ExceptionRaisedInUnexpectedLocation:
active: true
methodNames: [toString, hashCode, equals, finalize]
InstanceOfCheckForException:
active: false
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
NotImplementedDeclaration:
active: false
ObjectExtendsThrowable:
active: false
PrintStackTrace:
active: true
RethrowCaughtException:
active: true
ReturnFromFinally:
active: true
ignoreLabeled: false
SwallowedException:
active: true
ignoredExceptionTypes:
- InterruptedException
- NumberFormatException
- ParseException
- MalformedURLException
allowedExceptionNameRegex: '_|(ignore|expected).*'
ThrowingExceptionFromFinally:
active: true
ThrowingExceptionInMain:
active: false
ThrowingExceptionsWithoutMessageOrCause:
active: true
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
exceptions:
- IllegalArgumentException
- IllegalStateException
- IOException
ThrowingNewInstanceOfSameException:
active: true
TooGenericExceptionCaught:
active: true
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
exceptionNames:
- ArrayIndexOutOfBoundsException
- Error
- Exception
- IllegalMonitorStateException
- NullPointerException
- IndexOutOfBoundsException
- RuntimeException
- Throwable
allowedExceptionNameRegex: '_|(ignore|expected).*'
TooGenericExceptionThrown:
active: true
exceptionNames:
- Error
- Exception
- Throwable
- RuntimeException
formatting: formatting:
active: true
android: false
autoCorrect: true
AnnotationOnSeparateLine:
active: false
autoCorrect: true
AnnotationSpacing:
active: false
autoCorrect: true
ArgumentListWrapping:
active: false
autoCorrect: true
ChainWrapping:
active: true
autoCorrect: true
CommentSpacing:
active: true
autoCorrect: true
EnumEntryNameCase:
active: false
autoCorrect: true
Filename:
active: true
FinalNewline:
active: true
autoCorrect: true
insertFinalNewLine: true
ImportOrdering:
active: false
autoCorrect: true
layout: 'idea'
Indentation:
active: false
autoCorrect: true
indentSize: 4
continuationIndentSize: 4
MaximumLineLength:
active: true
maxLineLength: 120
ModifierOrdering:
active: true
autoCorrect: true
MultiLineIfElse:
active: true
autoCorrect: true
NoBlankLineBeforeRbrace:
active: true
autoCorrect: true
NoConsecutiveBlankLines:
active: true
autoCorrect: true
NoEmptyClassBody:
active: true
autoCorrect: true
NoEmptyFirstLineInMethodBlock:
active: false
autoCorrect: true
NoLineBreakAfterElse:
active: true
autoCorrect: true
NoLineBreakBeforeAssignment:
active: true
autoCorrect: true
NoMultipleSpaces:
active: true
autoCorrect: true
NoSemicolons:
active: true
autoCorrect: true
NoTrailingSpaces:
active: true
autoCorrect: true
NoUnitReturn:
active: true
autoCorrect: true
NoUnusedImports:
active: true
autoCorrect: true
NoWildcardImports:
active: true
PackageName:
active: true
autoCorrect: true
ParameterListWrapping: ParameterListWrapping:
active: false active: false
autoCorrect: true
indentSize: 4
SpacingAroundAngleBrackets:
active: false
autoCorrect: true
SpacingAroundColon:
active: true
autoCorrect: true
SpacingAroundComma:
active: true
autoCorrect: true
SpacingAroundCurly:
active: true
autoCorrect: true
SpacingAroundDot:
active: true
autoCorrect: true
SpacingAroundDoubleColon:
active: false
autoCorrect: true
SpacingAroundKeyword:
active: true
autoCorrect: true
SpacingAroundOperators:
active: true
autoCorrect: true
SpacingAroundParens:
active: true
autoCorrect: true
SpacingAroundRangeOperator:
active: true
autoCorrect: true
SpacingAroundUnaryOperator:
active: false
autoCorrect: true
SpacingBetweenDeclarationsWithAnnotations:
active: false
autoCorrect: true
SpacingBetweenDeclarationsWithComments:
active: false
autoCorrect: true
StringTemplate:
active: true
autoCorrect: true
naming:
active: true
ClassNaming:
active: true
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
classPattern: '[A-Z][a-zA-Z0-9]*'
ConstructorParameterNaming:
active: false
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
parameterPattern: '[a-z][A-Za-z0-9]*'
privateParameterPattern: '[a-z][A-Za-z0-9]*'
excludeClassPattern: '$^'
ignoreOverridden: true
EnumNaming:
active: true
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
enumEntryPattern: '[A-Z][_a-zA-Z0-9]*'
ForbiddenClassName:
active: false
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
forbiddenName: []
FunctionMaxLength:
active: false
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
maximumFunctionNameLength: 30
FunctionMinLength:
active: false
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
minimumFunctionNameLength: 3
FunctionNaming:
active: true
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
functionPattern: '([a-z][a-zA-Z0-9]*)|(`.*`)'
excludeClassPattern: '$^'
ignoreOverridden: true
ignoreAnnotated: ['Composable']
FunctionParameterNaming:
active: true
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
parameterPattern: '[a-z][A-Za-z0-9]*'
excludeClassPattern: '$^'
ignoreOverridden: true
InvalidPackageDeclaration:
active: false
excludes: ['*.kts']
rootPackage: ''
MatchingDeclarationName:
active: true
mustBeFirst: true
MemberNameEqualsClassName:
active: true
ignoreOverridden: true
NonBooleanPropertyPrefixedWithIs:
active: false
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
ObjectPropertyNaming:
active: true
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
constantPattern: '[A-Za-z][_A-Za-z0-9]*'
propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*'
PackageNaming:
active: true
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*'
TopLevelPropertyNaming:
active: true
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
constantPattern: '[A-Z][_A-Z0-9]*'
propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*'
VariableMaxLength:
active: false
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
maximumVariableNameLength: 64
VariableMinLength:
active: false
excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
minimumVariableNameLength: 1
VariableNaming:
active: true
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
variablePattern: '[a-z][A-Za-z0-9]*'
privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*'
excludeClassPattern: '$^'
ignoreOverridden: true
performance:
active: true
ArrayPrimitive:
active: true
ForEachOnRange:
active: true
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
SpreadOperator:
active: true
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
UnnecessaryTemporaryInstantiation:
active: true
potential-bugs:
active: true
Deprecation:
active: false
DontDowncastCollectionTypes:
active: false
DuplicateCaseInWhenExpression:
active: true
EqualsAlwaysReturnsTrueOrFalse:
active: true
EqualsWithHashCodeExist:
active: true
ExitOutsideMain:
active: false
ExplicitGarbageCollectionCall:
active: true
HasPlatformType:
active: false
IgnoredReturnValue:
active: false
restrictToAnnotatedMethods: true
returnValueAnnotations: ['*.CheckReturnValue', '*.CheckResult']
ImplicitDefaultLocale:
active: true
ImplicitUnitReturnType:
active: false
allowExplicitReturnType: true
InvalidRange:
active: true
IteratorHasNextCallsNextMethod:
active: true
IteratorNotThrowingNoSuchElementException:
active: true
LateinitUsage:
active: false
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
excludeAnnotatedProperties: []
ignoreOnClassesPattern: ''
MapGetWithNotNullAssertionOperator:
active: false
MissingWhenCase:
active: true
allowElseExpression: true
NullableToStringCall:
active: false
RedundantElseInWhen:
active: true
UnconditionalJumpStatementInLoop:
active: false
UnnecessaryNotNullOperator:
active: true
UnnecessarySafeCall:
active: true
UnreachableCatchBlock:
active: false
UnreachableCode:
active: true
UnsafeCallOnNullableType:
active: true
UnsafeCast:
active: true
UselessPostfixExpression:
active: false
WrongEqualsTypeParameter:
active: true
style: style:
active: true
ClassOrdering:
active: false
CollapsibleIfStatements:
active: false
DataClassContainsFunctions:
active: false
conversionFunctionPrefix: 'to'
DataClassShouldBeImmutable:
active: false
DestructuringDeclarationWithTooManyEntries:
active: false
maxDestructuringEntries: 3
EqualsNullCall:
active: true
EqualsOnSignatureLine:
active: false
ExplicitCollectionElementAccessMethod:
active: false
ExplicitItLambdaParameter:
active: false
ExpressionBodySyntax:
active: false
includeLineWrapping: false
ForbiddenComment:
active: true
values: ['TODO:', 'FIXME:', 'STOPSHIP:']
allowedPatterns: ''
ForbiddenImport:
active: false
imports: []
forbiddenPatterns: ''
ForbiddenMethodCall:
active: false
methods: ['kotlin.io.println', 'kotlin.io.print']
ForbiddenPublicDataClass:
active: true
excludes: ['**']
ignorePackages: ['*.internal', '*.internal.*']
ForbiddenVoid:
active: false
ignoreOverridden: false
ignoreUsageInGenerics: false
FunctionOnlyReturningConstant:
active: true
ignoreOverridableFunction: true
ignoreActualFunction: true
excludedFunctions: 'describeContents'
excludeAnnotatedFunction: ['dagger.Provides']
LibraryCodeMustSpecifyReturnType:
active: true
excludes: ['**']
LibraryEntitiesShouldNotBePublic:
active: true
excludes: ['**']
LoopWithTooManyJumpStatements:
active: true
maxJumpCount: 1
MagicNumber:
active: true
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
ignoreNumbers: ['-1', '0', '1', '2']
ignoreHashCodeFunction: true
ignorePropertyDeclaration: false
ignoreLocalVariableDeclaration: false
ignoreConstantDeclaration: true
ignoreCompanionObjectPropertyDeclaration: true
ignoreAnnotation: false
ignoreNamedArgument: true
ignoreEnums: false
ignoreRanges: false
ignoreExtensionFunctions: true
MandatoryBracesIfStatements:
active: false
MandatoryBracesLoops:
active: false
MaxLineLength: MaxLineLength:
excludes: ['**/test/**/*', '**/testIntegration/**/*']
active: true active: true
maxLineLength: 120 maxLineLength: 120
excludePackageStatements: true naming:
excludeImportStatements: true ConstructorParameterNaming:
excludeCommentStatements: false
MayBeConst:
active: true
ModifierOrder:
active: true
MultilineLambdaItParameter:
active: false active: false
NestedClassesVisibility:
active: true
NewLineAtEndOfFile:
active: true
NoTabs:
active: false
OptionalAbstractKeyword:
active: true
OptionalUnit:
active: false
OptionalWhenBraces:
active: false
PreferToOverPairSyntax:
active: false
ProtectedMemberInFinalClass:
active: true
RedundantExplicitType:
active: false
RedundantHigherOrderMapUsage:
active: false
RedundantVisibilityModifierRule:
active: false
ReturnCount:
active: true
max: 2
excludedFunctions: 'equals'
excludeLabeled: false
excludeReturnFromLambda: true
excludeGuardClauses: false
SafeCast:
active: true
SerialVersionUIDInSerializableClass:
active: true
SpacingBetweenPackageAndImports:
active: false
ThrowsCount:
active: true
max: 2
TrailingWhitespace:
active: false
UnderscoresInNumericLiterals:
active: false
acceptableDecimalLength: 5
UnnecessaryAbstractClass:
active: true
excludeAnnotatedClasses: ['dagger.Module']
UnnecessaryAnnotationUseSiteTarget:
active: false
UnnecessaryApply:
active: true
UnnecessaryFilter:
active: false
UnnecessaryInheritance:
active: true
UnnecessaryLet:
active: false
UnnecessaryParentheses:
active: false
UntilInsteadOfRangeTo:
active: false
UnusedImports:
active: false
UnusedPrivateClass:
active: true
UnusedPrivateMember:
active: true
allowedNames: '(_|ignored|expected|serialVersionUID)'
UseArrayLiteralsInAnnotations:
active: false
UseCheckNotNull:
active: false
UseCheckOrError:
active: false
UseDataClass:
active: false
excludeAnnotatedClasses: []
allowVars: false
UseEmptyCounterpart:
active: false
UseIfEmptyOrIfBlank:
active: false
UseIfInsteadOfWhen:
active: false
UseIsNullOrEmpty:
active: false
UseRequire:
active: false
UseRequireNotNull:
active: false
UselessCallOnNotNull:
active: true
UtilityClassWithPublicConstructor:
active: true
VarCouldBeVal:
active: true
WildcardImport:
active: true
excludes: ['**/test/**', '**/testIntegration/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
excludeImports: ['java.util.*', 'kotlinx.android.synthetic.*']

View File

@ -1,5 +1,5 @@
# Kompendium # Kompendium
project.version=1.3.0 project.version=1.10.0
# Kotlin # Kotlin
kotlin.code.style=official kotlin.code.style=official
# Gradle # Gradle

View File

@ -1,6 +1,6 @@
[versions] [versions]
kotlin = "1.4.32" kotlin = "1.4.32"
ktor = "1.5.3" ktor = "1.6.4"
kotlinx-serialization = "1.2.1" kotlinx-serialization = "1.2.1"
jackson-kotlin = "2.12.0" jackson-kotlin = "2.12.0"
slf4j = "1.7.30" slf4j = "1.7.30"

View File

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

View File

@ -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)
}
}

View File

@ -7,11 +7,14 @@ import io.ktor.auth.jwt.jwt
import io.ktor.auth.jwt.JWTAuthenticationProvider import io.ktor.auth.jwt.JWTAuthenticationProvider
import io.bkbn.kompendium.Kompendium import io.bkbn.kompendium.Kompendium
import io.bkbn.kompendium.models.oas.OpenApiSpecSchemaSecurity import io.bkbn.kompendium.models.oas.OpenApiSpecSchemaSecurity
import io.ktor.auth.AuthenticationRouteSelector
object KompendiumAuth { object KompendiumAuth {
init { init {
Kompendium.pathCalculator = AuthPathCalculator() Kompendium.addCustomRouteHandler(AuthenticationRouteSelector::class) { route, tail ->
calculate(route.parent, tail)
}
} }
fun Authentication.Configuration.notarizedBasic( fun Authentication.Configuration.notarizedBasic(

View File

@ -4,8 +4,12 @@ import io.bkbn.kompendium.models.meta.ErrorMap
import io.bkbn.kompendium.models.meta.SchemaMap import io.bkbn.kompendium.models.meta.SchemaMap
import io.bkbn.kompendium.models.oas.OpenApiSpec import io.bkbn.kompendium.models.oas.OpenApiSpec
import io.bkbn.kompendium.models.oas.OpenApiSpecInfo import io.bkbn.kompendium.models.oas.OpenApiSpecInfo
import io.bkbn.kompendium.path.CorePathCalculator import io.bkbn.kompendium.models.oas.TypedSchema
import io.bkbn.kompendium.path.IPathCalculator
import io.bkbn.kompendium.path.PathCalculator import io.bkbn.kompendium.path.PathCalculator
import io.ktor.routing.Route
import io.ktor.routing.RouteSelector
import kotlin.reflect.KClass
/** /**
* Maintains all state for the Kompendium library * Maintains all state for the Kompendium library
@ -21,7 +25,7 @@ object Kompendium {
paths = mutableMapOf() paths = mutableMapOf()
) )
var pathCalculator: PathCalculator = CorePathCalculator() fun calculatePath(route: Route) = PathCalculator.calculate(route)
fun resetSchema() { fun resetSchema() {
openApiSpec = OpenApiSpec( openApiSpec = OpenApiSpec(
@ -31,4 +35,15 @@ object Kompendium {
) )
cache = emptyMap() cache = emptyMap()
} }
fun addCustomTypeSchema(clazz: KClass<*>, schema: TypedSchema) {
cache = cache.plus(clazz.simpleName!! to schema)
}
fun <T : RouteSelector> addCustomRouteHandler(
selector: KClass<T>,
handler: IPathCalculator.(Route, String) -> String
) {
PathCalculator.addCustomRouteHandler(selector, handler)
}
} }

View File

@ -1,9 +1,7 @@
package io.bkbn.kompendium package io.bkbn.kompendium
import io.ktor.routing.Route import io.ktor.routing.Route
import kotlin.reflect.KClass
import kotlin.reflect.KType import kotlin.reflect.KType
import kotlin.reflect.full.createType
import kotlin.reflect.typeOf import kotlin.reflect.typeOf
/** /**
@ -49,23 +47,8 @@ object KompendiumPreFlight {
} }
fun addToCache(paramType: KType, requestType: KType, responseType: KType) { fun addToCache(paramType: KType, requestType: KType, responseType: KType) {
gatherSubTypes(requestType).forEach { Kompendium.cache = Kontent.generateKontent(requestType, Kompendium.cache)
Kompendium.cache = Kontent.generateKontent(it, Kompendium.cache) Kompendium.cache = Kontent.generateKontent(responseType, Kompendium.cache)
}
gatherSubTypes(responseType).forEach {
Kompendium.cache = Kontent.generateKontent(it, Kompendium.cache)
}
Kompendium.cache = Kontent.generateParameterKontent(paramType, 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)
}
}
} }

View File

@ -1,5 +1,6 @@
package io.bkbn.kompendium package io.bkbn.kompendium
import io.bkbn.kompendium.annotations.UndeclaredField
import io.bkbn.kompendium.models.meta.SchemaMap import io.bkbn.kompendium.models.meta.SchemaMap
import io.bkbn.kompendium.models.oas.AnyOfReferencedSchema import io.bkbn.kompendium.models.oas.AnyOfReferencedSchema
import io.bkbn.kompendium.models.oas.ArraySchema import io.bkbn.kompendium.models.oas.ArraySchema
@ -14,6 +15,9 @@ import io.bkbn.kompendium.util.Helpers.genericNameAdapter
import io.bkbn.kompendium.util.Helpers.getReferenceSlug import io.bkbn.kompendium.util.Helpers.getReferenceSlug
import io.bkbn.kompendium.util.Helpers.getSimpleSlug import io.bkbn.kompendium.util.Helpers.getSimpleSlug
import io.bkbn.kompendium.util.Helpers.logged import io.bkbn.kompendium.util.Helpers.logged
import org.slf4j.LoggerFactory
import java.math.BigDecimal
import java.math.BigInteger
import java.util.UUID import java.util.UUID
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.reflect.KType import kotlin.reflect.KType
@ -22,7 +26,6 @@ import kotlin.reflect.full.isSubclassOf
import kotlin.reflect.full.memberProperties import kotlin.reflect.full.memberProperties
import kotlin.reflect.jvm.javaField import kotlin.reflect.jvm.javaField
import kotlin.reflect.typeOf 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. * Responsible for generating the schema map that is used to power all object references across the API Spec.
@ -55,7 +58,22 @@ object Kontent {
type: KType, type: KType,
cache: SchemaMap = emptyMap() cache: SchemaMap = emptyMap()
): SchemaMap { ): SchemaMap {
return generateKTypeKontent(type, cache) var newCache = cache
gatherSubTypes(type).forEach {
newCache = generateKTypeKontent(it, newCache)
}
return newCache
}
private fun gatherSubTypes(type: KType): List<KType> {
val classifier = type.classifier as KClass<*>
return if (classifier.isSealed) {
classifier.sealedSubclasses.map {
it.createType(type.arguments)
}
} else {
listOf(type)
}
} }
/** /**
@ -106,6 +124,9 @@ object Kontent {
String::class -> cache.plus(clazz.simpleName!! to SimpleSchema("string")) String::class -> cache.plus(clazz.simpleName!! to SimpleSchema("string"))
Boolean::class -> cache.plus(clazz.simpleName!! to SimpleSchema("boolean")) Boolean::class -> cache.plus(clazz.simpleName!! to SimpleSchema("boolean"))
UUID::class -> cache.plus(clazz.simpleName!! to FormatSchema("uuid", "string")) UUID::class -> cache.plus(clazz.simpleName!! to FormatSchema("uuid", "string"))
BigDecimal::class -> cache.plus(clazz.simpleName!! to FormatSchema("double", "number"))
BigInteger::class -> cache.plus(clazz.simpleName!! to FormatSchema("int64", "integer"))
ByteArray::class -> cache.plus(clazz.simpleName!! to FormatSchema("byte", "string"))
else -> when { else -> when {
clazz.isSubclassOf(Collection::class) -> handleCollectionType(type, clazz, cache) clazz.isSubclassOf(Collection::class) -> handleCollectionType(type, clazz, cache)
clazz.isSubclassOf(Enum::class) -> handleEnumType(clazz, cache) clazz.isSubclassOf(Enum::class) -> handleEnumType(clazz, cache)
@ -120,6 +141,8 @@ object Kontent {
* @param clazz Class of the object to analyze * @param clazz Class of the object to analyze
* @param cache Existing schema map to append to * @param cache Existing schema map to append to
*/ */
// TODO Fix as part of this issue https://github.com/bkbnio/kompendium/issues/80
@Suppress("LongMethod", "ComplexMethod")
private fun handleComplexType(type: KType, clazz: KClass<*>, cache: SchemaMap): SchemaMap { 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 // This needs to be simple because it will be stored under it's appropriate reference component implicitly
val slug = type.getSimpleSlug() val slug = type.getSimpleSlug()
@ -185,8 +208,14 @@ object Kontent {
} }
Pair(prop.name, propSchema) Pair(prop.name, propSchema)
} }
logger.debug("Looking for undeclared fields")
val undeclaredFieldMap = clazz.annotations.filterIsInstance<UndeclaredField>().associate {
val undeclaredType = it.clazz.createType()
newCache = generateKontent(undeclaredType, newCache)
it.field to ReferencedSchema(undeclaredType.getReferenceSlug())
}
logger.debug("$slug contains $fieldMap") logger.debug("$slug contains $fieldMap")
val schema = ObjectSchema(fieldMap) val schema = ObjectSchema(fieldMap.plus(undeclaredFieldMap))
logger.debug("$slug schema: $schema") logger.debug("$slug schema: $schema")
newCache.plus(slug to schema) newCache.plus(slug to schema)
} }
@ -216,11 +245,20 @@ object Kontent {
if (keyType?.classifier != String::class) { if (keyType?.classifier != String::class) {
error("Invalid Map $type: OpenAPI dictionaries must have keys of type String") error("Invalid Map $type: OpenAPI dictionaries must have keys of type String")
} }
val valClassName = (valType?.classifier as KClass<*>).simpleName val valClass = valType?.classifier as KClass<*>
val valClassName = valClass.simpleName
val referenceName = genericNameAdapter(type, clazz) val referenceName = genericNameAdapter(type, clazz)
val valueReference = ReferencedSchema("$COMPONENT_SLUG/$valClassName") val valueReference = when (valClass.isSealed) {
true -> {
val subTypes = gatherSubTypes(valType)
AnyOfReferencedSchema(subTypes.map {
ReferencedSchema(("$COMPONENT_SLUG/${it.getSimpleSlug()}"))
})
}
false -> ReferencedSchema("$COMPONENT_SLUG/$valClassName")
}
val schema = DictionarySchema(additionalProperties = valueReference) val schema = DictionarySchema(additionalProperties = valueReference)
val updatedCache = generateKTypeKontent(valType, cache) val updatedCache = generateKontent(valType, cache)
return updatedCache.plus(referenceName to schema) return updatedCache.plus(referenceName to schema)
} }
@ -236,9 +274,17 @@ object Kontent {
val collectionClass = collectionType.classifier as KClass<*> val collectionClass = collectionType.classifier as KClass<*>
logger.debug("Obtained collection class: $collectionClass") logger.debug("Obtained collection class: $collectionClass")
val referenceName = genericNameAdapter(type, clazz) val referenceName = genericNameAdapter(type, clazz)
val valueReference = ReferencedSchema("${COMPONENT_SLUG}/${collectionClass.simpleName}") val valueReference = when (collectionClass.isSealed) {
true -> {
val subTypes = gatherSubTypes(collectionType)
AnyOfReferencedSchema(subTypes.map {
ReferencedSchema(("$COMPONENT_SLUG/${it.getSimpleSlug()}"))
})
}
false -> ReferencedSchema("$COMPONENT_SLUG/${collectionClass.simpleName}")
}
val schema = ArraySchema(items = valueReference) val schema = ArraySchema(items = valueReference)
val updatedCache = generateKTypeKontent(collectionType, cache) val updatedCache = generateKontent(collectionType, cache)
return updatedCache.plus(referenceName to schema) return updatedCache.plus(referenceName to schema)
} }
} }

View File

@ -49,6 +49,7 @@ object MethodParser {
) = OpenApiSpecPathItemOperation( ) = OpenApiSpecPathItemOperation(
summary = info.summary, summary = info.summary,
description = info.description, description = info.description,
operationId = info.operationId,
tags = info.tags, tags = info.tags,
deprecated = info.deprecated, deprecated = info.deprecated,
parameters = paramType.toParameterSpec(), parameters = paramType.toParameterSpec(),

View File

@ -36,7 +36,7 @@ object Notarized {
noinline body: PipelineInterceptor<Unit, ApplicationCall> noinline body: PipelineInterceptor<Unit, ApplicationCall>
): Route = ): Route =
KompendiumPreFlight.methodNotarizationPreFlight<TParam, Unit, TResp>() { paramType, requestType, responseType -> KompendiumPreFlight.methodNotarizationPreFlight<TParam, Unit, TResp>() { paramType, requestType, responseType ->
val path = Kompendium.pathCalculator.calculate(this) val path = Kompendium.calculatePath(this)
Kompendium.openApiSpec.paths.getOrPut(path) { OpenApiSpecPathItem() } Kompendium.openApiSpec.paths.getOrPut(path) { OpenApiSpecPathItem() }
Kompendium.openApiSpec.paths[path]?.get = parseMethodInfo(info, paramType, requestType, responseType) Kompendium.openApiSpec.paths[path]?.get = parseMethodInfo(info, paramType, requestType, responseType)
return method(HttpMethod.Get) { handle(body) } return method(HttpMethod.Get) { handle(body) }
@ -55,7 +55,7 @@ object Notarized {
noinline body: PipelineInterceptor<Unit, ApplicationCall> noinline body: PipelineInterceptor<Unit, ApplicationCall>
): Route = ): Route =
KompendiumPreFlight.methodNotarizationPreFlight<TParam, TReq, TResp>() { paramType, requestType, responseType -> KompendiumPreFlight.methodNotarizationPreFlight<TParam, TReq, TResp>() { paramType, requestType, responseType ->
val path = Kompendium.pathCalculator.calculate(this) val path = Kompendium.calculatePath(this)
Kompendium.openApiSpec.paths.getOrPut(path) { OpenApiSpecPathItem() } Kompendium.openApiSpec.paths.getOrPut(path) { OpenApiSpecPathItem() }
Kompendium.openApiSpec.paths[path]?.post = parseMethodInfo(info, paramType, requestType, responseType) Kompendium.openApiSpec.paths[path]?.post = parseMethodInfo(info, paramType, requestType, responseType)
return method(HttpMethod.Post) { handle(body) } return method(HttpMethod.Post) { handle(body) }
@ -74,7 +74,7 @@ object Notarized {
noinline body: PipelineInterceptor<Unit, ApplicationCall>, noinline body: PipelineInterceptor<Unit, ApplicationCall>,
): Route = ): Route =
KompendiumPreFlight.methodNotarizationPreFlight<TParam, TReq, TResp>() { paramType, requestType, responseType -> KompendiumPreFlight.methodNotarizationPreFlight<TParam, TReq, TResp>() { paramType, requestType, responseType ->
val path = Kompendium.pathCalculator.calculate(this) val path = Kompendium.calculatePath(this)
Kompendium.openApiSpec.paths.getOrPut(path) { OpenApiSpecPathItem() } Kompendium.openApiSpec.paths.getOrPut(path) { OpenApiSpecPathItem() }
Kompendium.openApiSpec.paths[path]?.put = Kompendium.openApiSpec.paths[path]?.put =
parseMethodInfo(info, paramType, requestType, responseType) parseMethodInfo(info, paramType, requestType, responseType)
@ -93,7 +93,7 @@ object Notarized {
noinline body: PipelineInterceptor<Unit, ApplicationCall> noinline body: PipelineInterceptor<Unit, ApplicationCall>
): Route = ): Route =
KompendiumPreFlight.methodNotarizationPreFlight<TParam, Unit, TResp> { paramType, requestType, responseType -> KompendiumPreFlight.methodNotarizationPreFlight<TParam, Unit, TResp> { paramType, requestType, responseType ->
val path = Kompendium.pathCalculator.calculate(this) val path = Kompendium.calculatePath(this)
Kompendium.openApiSpec.paths.getOrPut(path) { OpenApiSpecPathItem() } Kompendium.openApiSpec.paths.getOrPut(path) { OpenApiSpecPathItem() }
Kompendium.openApiSpec.paths[path]?.delete = parseMethodInfo(info, paramType, requestType, responseType) Kompendium.openApiSpec.paths[path]?.delete = parseMethodInfo(info, paramType, requestType, responseType)
return method(HttpMethod.Delete) { handle(body) } return method(HttpMethod.Delete) { handle(body) }
@ -112,5 +112,4 @@ object Notarized {
info.parseErrorInfo(errorType, responseType) info.parseErrorInfo(errorType, responseType)
exception(handler) exception(handler)
} }
} }

View File

@ -0,0 +1,8 @@
package io.bkbn.kompendium.annotations
import kotlin.reflect.KClass
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
@Repeatable
annotation class UndeclaredField(val field: String, val clazz: KClass<*>)

View File

@ -11,6 +11,7 @@ sealed class MethodInfo<TParam, TResp>(
open val canThrow: Set<KClass<*>> = emptySet(), open val canThrow: Set<KClass<*>> = emptySet(),
open val responseInfo: ResponseInfo<TResp>? = null, open val responseInfo: ResponseInfo<TResp>? = null,
open val parameterExamples: Map<String, TParam> = emptyMap(), open val parameterExamples: Map<String, TParam> = emptyMap(),
open val operationId: String? = null
) { ) {
data class GetInfo<TParam, TResp>( data class GetInfo<TParam, TResp>(
@ -21,7 +22,8 @@ sealed class MethodInfo<TParam, TResp>(
override val deprecated: Boolean = false, override val deprecated: Boolean = false,
override val securitySchemes: Set<String> = emptySet(), override val securitySchemes: Set<String> = emptySet(),
override val canThrow: Set<KClass<*>> = emptySet(), override val canThrow: Set<KClass<*>> = emptySet(),
override val parameterExamples: Map<String, TParam> = emptyMap() override val parameterExamples: Map<String, TParam> = emptyMap(),
override val operationId: String? = null
) : MethodInfo<TParam, TResp>( ) : MethodInfo<TParam, TResp>(
summary = summary, summary = summary,
description = description, description = description,
@ -30,7 +32,8 @@ sealed class MethodInfo<TParam, TResp>(
securitySchemes = securitySchemes, securitySchemes = securitySchemes,
canThrow = canThrow, canThrow = canThrow,
responseInfo = responseInfo, responseInfo = responseInfo,
parameterExamples = parameterExamples parameterExamples = parameterExamples,
operationId = operationId
) )
data class PostInfo<TParam, TReq, TResp>( data class PostInfo<TParam, TReq, TResp>(
@ -42,7 +45,8 @@ sealed class MethodInfo<TParam, TResp>(
override val deprecated: Boolean = false, override val deprecated: Boolean = false,
override val securitySchemes: Set<String> = emptySet(), override val securitySchemes: Set<String> = emptySet(),
override val canThrow: Set<KClass<*>> = emptySet(), override val canThrow: Set<KClass<*>> = emptySet(),
override val parameterExamples: Map<String, TParam> = emptyMap() override val parameterExamples: Map<String, TParam> = emptyMap(),
override val operationId: String? = null
) : MethodInfo<TParam, TResp>( ) : MethodInfo<TParam, TResp>(
summary = summary, summary = summary,
description = description, description = description,
@ -51,7 +55,8 @@ sealed class MethodInfo<TParam, TResp>(
securitySchemes = securitySchemes, securitySchemes = securitySchemes,
canThrow = canThrow, canThrow = canThrow,
responseInfo = responseInfo, responseInfo = responseInfo,
parameterExamples = parameterExamples parameterExamples = parameterExamples,
operationId = operationId
) )
data class PutInfo<TParam, TReq, TResp>( data class PutInfo<TParam, TReq, TResp>(
@ -63,7 +68,8 @@ sealed class MethodInfo<TParam, TResp>(
override val deprecated: Boolean = false, override val deprecated: Boolean = false,
override val securitySchemes: Set<String> = emptySet(), override val securitySchemes: Set<String> = emptySet(),
override val canThrow: Set<KClass<*>> = emptySet(), override val canThrow: Set<KClass<*>> = emptySet(),
override val parameterExamples: Map<String, TParam> = emptyMap() override val parameterExamples: Map<String, TParam> = emptyMap(),
override val operationId: String? = null
) : MethodInfo<TParam, TResp>( ) : MethodInfo<TParam, TResp>(
summary = summary, summary = summary,
description = description, description = description,
@ -71,7 +77,8 @@ sealed class MethodInfo<TParam, TResp>(
deprecated = deprecated, deprecated = deprecated,
securitySchemes = securitySchemes, securitySchemes = securitySchemes,
canThrow = canThrow, canThrow = canThrow,
parameterExamples = parameterExamples parameterExamples = parameterExamples,
operationId = operationId
) )
data class DeleteInfo<TParam, TResp>( data class DeleteInfo<TParam, TResp>(
@ -82,7 +89,8 @@ sealed class MethodInfo<TParam, TResp>(
override val deprecated: Boolean = false, override val deprecated: Boolean = false,
override val securitySchemes: Set<String> = emptySet(), override val securitySchemes: Set<String> = emptySet(),
override val canThrow: Set<KClass<*>> = emptySet(), override val canThrow: Set<KClass<*>> = emptySet(),
override val parameterExamples: Map<String, TParam> = emptyMap() override val parameterExamples: Map<String, TParam> = emptyMap(),
override val operationId: String? = null
) : MethodInfo<TParam, TResp>( ) : MethodInfo<TParam, TResp>(
summary = summary, summary = summary,
description = description, description = description,
@ -90,6 +98,7 @@ sealed class MethodInfo<TParam, TResp>(
deprecated = deprecated, deprecated = deprecated,
securitySchemes = securitySchemes, securitySchemes = securitySchemes,
canThrow = canThrow, canThrow = canThrow,
parameterExamples = parameterExamples parameterExamples = parameterExamples,
operationId = operationId
) )
} }

View File

@ -1,7 +1,6 @@
package io.bkbn.kompendium.models.oas package io.bkbn.kompendium.models.oas
sealed class OpenApiSpecComponentSchema(open val default: Any? = null) { sealed class OpenApiSpecComponentSchema(open val default: Any? = null) {
fun addDefault(default: Any?): OpenApiSpecComponentSchema = when (this) { fun addDefault(default: Any?): OpenApiSpecComponentSchema = when (this) {
is AnyOfReferencedSchema -> error("Cannot add default to anyOf reference") is AnyOfReferencedSchema -> error("Cannot add default to anyOf reference")
is ReferencedSchema -> this.copy(default = default) is ReferencedSchema -> this.copy(default = default)
@ -12,7 +11,6 @@ sealed class OpenApiSpecComponentSchema(open val default: Any? = null) {
is FormatSchema -> this.copy(default = default) is FormatSchema -> this.copy(default = default)
is ArraySchema -> this.copy(default = default) is ArraySchema -> this.copy(default = default)
} }
} }
sealed class TypedSchema(open val type: String, override val default: Any? = null) : OpenApiSpecComponentSchema(default) sealed class TypedSchema(open val type: String, override val default: Any? = null) : OpenApiSpecComponentSchema(default)

View File

@ -1,51 +0,0 @@
package io.bkbn.kompendium.path
import io.ktor.routing.PathSegmentConstantRouteSelector
import io.ktor.routing.PathSegmentParameterRouteSelector
import io.ktor.routing.RootRouteSelector
import io.ktor.routing.Route
import io.ktor.util.InternalAPI
import org.slf4j.LoggerFactory
/**
* Default [PathCalculator] meant to be overridden as necessary
*/
open class CorePathCalculator : PathCalculator {
private val logger = LoggerFactory.getLogger(javaClass)
@OptIn(InternalAPI::class)
override fun calculate(
route: Route?,
tail: String
): String = when (route) {
null -> tail
else -> when (route.selector) {
is RootRouteSelector -> {
logger.debug("Root route detected, returning path: $tail")
tail
}
is PathSegmentParameterRouteSelector -> {
logger.debug("Found segment parameter ${route.selector}, continuing to parent")
val newTail = "/${route.selector}$tail"
calculate(route.parent, newTail)
}
is PathSegmentConstantRouteSelector -> {
logger.debug("Found segment constant ${route.selector}, continuing to parent")
val newTail = "/${route.selector}$tail"
calculate(route.parent, newTail)
}
else -> when (route.selector.javaClass.simpleName) {
"TrailingSlashRouteSelector" -> {
logger.debug("Found trailing slash route selector")
val newTail = tail.ifBlank { "/" }
calculate(route.parent, newTail)
}
else -> handleCustomSelectors(route, tail)
}
}
}
override fun handleCustomSelectors(route: Route, tail: String): String = error("Unknown selector ${route.selector}")
}

View File

@ -0,0 +1,13 @@
package io.bkbn.kompendium.path
import io.ktor.routing.Route
import io.ktor.routing.RouteSelector
import kotlin.reflect.KClass
interface IPathCalculator {
fun calculate(route: Route?, tail: String = ""): String
fun <T : RouteSelector> addCustomRouteHandler(
selector: KClass<T>,
handler: IPathCalculator.(Route, String) -> String
)
}

View File

@ -1,20 +1,54 @@
package io.bkbn.kompendium.path package io.bkbn.kompendium.path
import io.ktor.routing.PathSegmentConstantRouteSelector
import io.ktor.routing.PathSegmentParameterRouteSelector
import io.ktor.routing.RootRouteSelector
import io.ktor.routing.Route import io.ktor.routing.Route
import io.ktor.routing.RouteSelector
import io.ktor.routing.TrailingSlashRouteSelector
import io.ktor.util.InternalAPI
import kotlin.reflect.KClass
/** /**
* Extensible interface for calculating Ktor paths * Responsible for calculating a url path from a provided [Route]
*/ */
interface PathCalculator { @OptIn(InternalAPI::class)
internal object PathCalculator: IPathCalculator {
/** private val pathHandler: RouteHandlerMap = mutableMapOf()
* Core route calculation method
*/
fun calculate(route: Route?, tail: String = ""): String
/** init {
* Used to handle any custom selectors that may be missed by the base route calculation addCustomRouteHandler(RootRouteSelector::class) { _, tail -> tail }
*/ addCustomRouteHandler(PathSegmentParameterRouteSelector::class) { route, tail ->
fun handleCustomSelectors(route: Route, tail: String): String val newTail = "/${route.selector}$tail"
calculate(route.parent, newTail)
}
addCustomRouteHandler(PathSegmentConstantRouteSelector::class) { route, tail ->
val newTail = "/${route.selector}$tail"
calculate(route.parent, newTail)
}
addCustomRouteHandler(TrailingSlashRouteSelector::class) { route, tail ->
val newTail = tail.ifBlank { "/" }
calculate(route.parent, newTail)
}
}
@OptIn(InternalAPI::class)
override fun calculate(
route: Route?,
tail: String
): String = when (route) {
null -> tail
else -> when (pathHandler.containsKey(route.selector::class)) {
true -> pathHandler[route.selector::class]!!.invoke(this, route, tail)
else -> error("No handler has been registered for ${route.selector}")
}
}
override fun <T : RouteSelector> addCustomRouteHandler(
selector: KClass<T>,
handler: IPathCalculator.(Route, String) -> String
) {
pathHandler[selector] = handler
}
} }

View File

@ -0,0 +1,7 @@
package io.bkbn.kompendium.path
import io.ktor.routing.Route
import io.ktor.routing.RouteSelector
import kotlin.reflect.KClass
typealias RouteHandlerMap = MutableMap<KClass<out RouteSelector>, IPathCalculator.(Route, String) -> String>

View File

@ -13,14 +13,19 @@ import io.ktor.routing.route
/** /**
* Provides an out-of-the-box route to return the generated [OpenApiSpec] * Provides an out-of-the-box route to return the generated [OpenApiSpec]
* @param oas spec that is returned * @param oas spec that is returned
* @param om provider for Jackson
*/ */
fun Routing.openApi(oas: OpenApiSpec) { fun Routing.openApi(
val om = ObjectMapper() oas: OpenApiSpec,
.setSerializationInclusion(JsonInclude.Include.NON_NULL) om: ObjectMapper = objectMapper
.enable(SerializationFeature.INDENT_OUTPUT) ) {
route("/openapi.json") { route("/openapi.json") {
get { get {
call.respondText { om.writeValueAsString(oas) } call.respondText { om.writeValueAsString(oas) }
} }
} }
} }
private val objectMapper = ObjectMapper()
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
.enable(SerializationFeature.INDENT_OUTPUT)

View File

@ -32,6 +32,9 @@ import io.bkbn.kompendium.util.notarizedGetWithNotarizedException
import io.bkbn.kompendium.util.notarizedPostModule import io.bkbn.kompendium.util.notarizedPostModule
import io.bkbn.kompendium.util.notarizedPutModule import io.bkbn.kompendium.util.notarizedPutModule
import io.bkbn.kompendium.util.pathParsingTestModule import io.bkbn.kompendium.util.pathParsingTestModule
import io.bkbn.kompendium.util.polymorphicCollectionResponse
import io.bkbn.kompendium.util.polymorphicInterfaceResponse
import io.bkbn.kompendium.util.polymorphicMapResponse
import io.bkbn.kompendium.util.polymorphicResponse import io.bkbn.kompendium.util.polymorphicResponse
import io.bkbn.kompendium.util.primitives import io.bkbn.kompendium.util.primitives
import io.bkbn.kompendium.util.returnsList import io.bkbn.kompendium.util.returnsList
@ -40,8 +43,10 @@ import io.bkbn.kompendium.util.simpleGenericResponse
import io.bkbn.kompendium.util.statusPageModule import io.bkbn.kompendium.util.statusPageModule
import io.bkbn.kompendium.util.statusPageMultiExceptions import io.bkbn.kompendium.util.statusPageMultiExceptions
import io.bkbn.kompendium.util.trailingSlash import io.bkbn.kompendium.util.trailingSlash
import io.bkbn.kompendium.util.undeclaredType
import io.bkbn.kompendium.util.withDefaultParameter import io.bkbn.kompendium.util.withDefaultParameter
import io.bkbn.kompendium.util.withExamples import io.bkbn.kompendium.util.withExamples
import io.bkbn.kompendium.util.withOperationId
internal class KompendiumTest { internal class KompendiumTest {
@ -135,7 +140,6 @@ internal class KompendiumTest {
} }
} }
@Test @Test
fun `Notarized put does not interrupt the pipeline`() { fun `Notarized put does not interrupt the pipeline`() {
withTestApplication({ withTestApplication({
@ -359,6 +363,22 @@ internal class KompendiumTest {
} }
} }
@Test
fun `Can add operationId`() {
withTestApplication({
jacksonConfigModule()
docs()
withOperationId()
}) {
// do
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = getFileSnapshot("notarized_get_with_operation_id.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@Test @Test
fun `Generates the expected redoc`() { fun `Generates the expected redoc`() {
withTestApplication({ withTestApplication({
@ -457,6 +477,54 @@ internal class KompendiumTest {
} }
} }
@Test
fun `Can generate a collection with polymorphic response type`() {
withTestApplication({
jacksonConfigModule()
docs()
polymorphicCollectionResponse()
}) {
// do
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = getFileSnapshot("polymorphic_list_response.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@Test
fun `Can generate a map with a polymorphic response type`() {
withTestApplication({
jacksonConfigModule()
docs()
polymorphicMapResponse()
}) {
// do
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = getFileSnapshot("polymorphic_map_response.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@Test
fun `Can generate a polymorphic response from a sealed interface`() {
withTestApplication({
jacksonConfigModule()
docs()
polymorphicInterfaceResponse()
}) {
// do
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = getFileSnapshot("sealed_interface_response.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
@Test @Test
fun `Can generate a response type with a generic type`() { fun `Can generate a response type with a generic type`() {
withTestApplication({ withTestApplication({
@ -505,6 +573,22 @@ internal class KompendiumTest {
} }
} }
@Test
fun `Can add an undeclared field`() {
withTestApplication({
kotlinxConfigModule()
docs()
undeclaredType()
}) {
// do
val json = handleRequest(HttpMethod.Get, "/openapi.json").response.content
// expect
val expected = getFileSnapshot("undeclared_field.json").trim()
assertEquals(expected, json, "The received json spec should match the expected content")
}
}
private val oas = Kompendium.openApiSpec.copy( private val oas = Kompendium.openApiSpec.copy(
info = OpenApiSpecInfo( info = OpenApiSpecInfo(
title = "Test API", title = "Test API",
@ -539,5 +623,4 @@ internal class KompendiumTest {
redoc(oas) redoc(oas)
} }
} }
} }

View File

@ -13,15 +13,7 @@ import io.bkbn.kompendium.models.oas.DictionarySchema
import io.bkbn.kompendium.models.oas.FormatSchema import io.bkbn.kompendium.models.oas.FormatSchema
import io.bkbn.kompendium.models.oas.ObjectSchema import io.bkbn.kompendium.models.oas.ObjectSchema
import io.bkbn.kompendium.models.oas.ReferencedSchema import io.bkbn.kompendium.models.oas.ReferencedSchema
import io.bkbn.kompendium.util.ComplexRequest import io.bkbn.kompendium.util.*
import io.bkbn.kompendium.util.TestInvalidMap
import io.bkbn.kompendium.util.TestNestedModel
import io.bkbn.kompendium.util.TestSimpleModel
import io.bkbn.kompendium.util.TestSimpleWithEnumList
import io.bkbn.kompendium.util.TestSimpleWithEnums
import io.bkbn.kompendium.util.TestSimpleWithList
import io.bkbn.kompendium.util.TestSimpleWithMap
import io.bkbn.kompendium.util.TestWithUUID
@ExperimentalStdlibApi @ExperimentalStdlibApi
internal class KontentTest { internal class KontentTest {
@ -45,6 +37,29 @@ internal class KontentTest {
assertEquals(FormatSchema("int64", "integer"), result["Long"]) assertEquals(FormatSchema("int64", "integer"), result["Long"])
} }
@Test
fun `Object with BigDecimal and BigInteger types`() {
// do
val result = generateKontent<TestBigNumberModel>()
// expect
assertEquals(3, result.count())
assertTrue { result.containsKey(TestBigNumberModel::class.simpleName) }
assertEquals(FormatSchema("double", "number"), result["BigDecimal"])
assertEquals(FormatSchema("int64", "integer"), result["BigInteger"])
}
@Test
fun `Object with ByteArray type`() {
// do
val result = generateKontent<TestByteArrayModel>()
// expect
assertEquals(2, result.count())
assertTrue { result.containsKey(TestByteArrayModel::class.simpleName) }
assertEquals(FormatSchema("byte", "string"), result["ByteArray"])
}
@Test @Test
fun `Objects reference their base types in the cache`() { fun `Objects reference their base types in the cache`() {
// do // do

View File

@ -4,9 +4,16 @@ import java.util.UUID
import io.bkbn.kompendium.annotations.KompendiumField import io.bkbn.kompendium.annotations.KompendiumField
import io.bkbn.kompendium.annotations.KompendiumParam import io.bkbn.kompendium.annotations.KompendiumParam
import io.bkbn.kompendium.annotations.ParamType import io.bkbn.kompendium.annotations.ParamType
import io.bkbn.kompendium.annotations.UndeclaredField
import java.math.BigDecimal
import java.math.BigInteger
data class TestSimpleModel(val a: String, val b: Int) data class TestSimpleModel(val a: String, val b: Int)
data class TestBigNumberModel(val a: BigDecimal, val b: BigInteger)
data class TestByteArrayModel(val a: ByteArray)
data class TestNestedModel(val inner: TestSimpleModel) data class TestNestedModel(val inner: TestSimpleModel)
data class TestSimpleWithEnums(val a: String, val b: SimpleEnum) data class TestSimpleWithEnums(val a: String, val b: SimpleEnum)
@ -80,7 +87,21 @@ sealed class FlibbityGibbit
data class SimpleGibbit(val a: String) : FlibbityGibbit() data class SimpleGibbit(val a: String) : FlibbityGibbit()
data class ComplexGibbit(val b: String, val c: Int) : FlibbityGibbit() data class ComplexGibbit(val b: String, val c: Int) : FlibbityGibbit()
sealed interface SlammaJamma
data class OneJamma(val a: Int) : SlammaJamma
data class AnothaJamma(val b: Float) : SlammaJamma
//data class InsaneJamma(val c: SlammaJamma) : SlammaJamma // 👀
sealed interface Flibbity<T> sealed interface Flibbity<T>
data class Gibbity<T>(val a: T): Flibbity<T> data class Gibbity<T>(val a: T) : Flibbity<T>
data class Bibbity<T>(val b: String, val f: T) : Flibbity<T> data class Bibbity<T>(val b: String, val f: T) : Flibbity<T>
enum class Hehe {
HAHA,
HOHO
}
@UndeclaredField("nowYouDont", Hehe::class)
data class Mysterious(val nowYouSeeMe: String)

View File

@ -265,6 +265,18 @@ fun Application.withDefaultParameter() {
} }
} }
fun Application.withOperationId(){
routing {
route("/test") {
notarizedGet(
info = TestResponseInfo.testGetInfo.copy(operationId = "getTest")
){
call.respond(HttpStatusCode.OK)
}
}
}
}
fun Application.nonRequiredParamsGet() { fun Application.nonRequiredParamsGet() {
routing { routing {
route("/test/optional") { route("/test/optional") {
@ -285,6 +297,36 @@ fun Application.polymorphicResponse() {
} }
} }
fun Application.polymorphicCollectionResponse() {
routing {
route("/test/polymorphiclist") {
notarizedGet(TestResponseInfo.polymorphicListResponse) {
call.respond(HttpStatusCode.OK, listOf(SimpleGibbit("hi")))
}
}
}
}
fun Application.polymorphicMapResponse() {
routing {
route("/test/polymorphicmap") {
notarizedGet(TestResponseInfo.polymorphicMapResponse) {
call.respond(HttpStatusCode.OK, listOf(SimpleGibbit("hi")))
}
}
}
}
fun Application.polymorphicInterfaceResponse() {
routing {
route("/test/polymorphicmap") {
notarizedGet(TestResponseInfo.polymorphicInterfaceResponse) {
call.respond(HttpStatusCode.OK, listOf(SimpleGibbit("hi")))
}
}
}
}
fun Application.genericPolymorphicResponse() { fun Application.genericPolymorphicResponse() {
routing { routing {
route("/test/polymorphic") { route("/test/polymorphic") {
@ -310,6 +352,16 @@ fun Application.genericPolymorphicResponseMultipleImpls() {
} }
} }
fun Application.undeclaredType() {
routing {
route("/test/polymorphic") {
notarizedGet(TestResponseInfo.undeclaredResponseType) {
call.respond(HttpStatusCode.OK, Mysterious("hi"))
}
}
}
}
fun Application.simpleGenericResponse() { fun Application.simpleGenericResponse() {
routing { routing {
route("/test/polymorphic") { route("/test/polymorphic") {

View File

@ -78,6 +78,21 @@ object TestResponseInfo {
description = "Polymorphic response", description = "Polymorphic response",
responseInfo = simpleOkResponse() responseInfo = simpleOkResponse()
) )
val polymorphicListResponse = GetInfo<Unit, List<FlibbityGibbit>>(
summary = "Oh so many gibbits",
description = "Polymorphic list response",
responseInfo = simpleOkResponse()
)
val polymorphicMapResponse = GetInfo<Unit, Map<String, FlibbityGibbit>>(
summary = "By gawd that's a lot of gibbits",
description = "Polymorphic list response",
responseInfo = simpleOkResponse()
)
val polymorphicInterfaceResponse = GetInfo<Unit, SlammaJamma>(
summary = "Come on and slam",
description = "and welcome to the jam",
responseInfo = simpleOkResponse()
)
val genericPolymorphicResponse = GetInfo<Unit, Flibbity<TestNested>>( val genericPolymorphicResponse = GetInfo<Unit, Flibbity<TestNested>>(
summary = "More flibbity", summary = "More flibbity",
description = "Polymorphic with generics", description = "Polymorphic with generics",
@ -88,6 +103,11 @@ object TestResponseInfo {
description = "Polymorphic with generics but like... crazier", description = "Polymorphic with generics but like... crazier",
responseInfo = simpleOkResponse() responseInfo = simpleOkResponse()
) )
val undeclaredResponseType = GetInfo<Unit, Mysterious>(
summary = "spooky class",
description = "break this glass in scenario of emergency",
responseInfo = simpleOkResponse()
)
val genericResponse = GetInfo<Unit, TestGeneric<Int>>( val genericResponse = GetInfo<Unit, TestGeneric<Int>>(
summary = "Single Generic", summary = "Single Generic",
description = "Simple generic data class", description = "Simple generic data class",

View File

@ -0,0 +1,88 @@
{
"openapi" : "3.0.3",
"info" : {
"title" : "Test API",
"version" : "1.33.7",
"description" : "An amazing, fully-ish 😉 generated API spec",
"termsOfService" : "https://example.com",
"contact" : {
"name" : "Homer Simpson",
"url" : "https://gph.is/1NPUDiM",
"email" : "chunkylover53@aol.com"
},
"license" : {
"name" : "MIT",
"url" : "https://github.com/bkbnio/kompendium/blob/main/LICENSE"
}
},
"servers" : [ {
"url" : "https://myawesomeapi.com",
"description" : "Production instance of my API"
}, {
"url" : "https://staging.myawesomeapi.com",
"description" : "Where the fun stuff happens"
} ],
"paths" : {
"/test" : {
"get" : {
"tags" : [ ],
"summary" : "Another get test",
"description" : "testing more",
"operationId" : "getTest",
"parameters" : [ {
"name" : "a",
"in" : "path",
"schema" : {
"type" : "string"
},
"required" : true,
"deprecated" : false
}, {
"name" : "aa",
"in" : "query",
"schema" : {
"type" : "integer",
"format" : "int32"
},
"required" : true,
"deprecated" : false
} ],
"responses" : {
"200" : {
"description" : "A Successful Endeavor",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/TestResponse"
}
}
}
}
},
"deprecated" : false
}
}
},
"components" : {
"schemas" : {
"String" : {
"type" : "string"
},
"TestResponse" : {
"type" : "object",
"properties" : {
"c" : {
"$ref" : "#/components/schemas/String"
}
}
},
"Int" : {
"type" : "integer",
"format" : "int32"
}
},
"securitySchemes" : { }
},
"security" : [ ],
"tags" : [ ]
}

View File

@ -0,0 +1,91 @@
{
"openapi" : "3.0.3",
"info" : {
"title" : "Test API",
"version" : "1.33.7",
"description" : "An amazing, fully-ish 😉 generated API spec",
"termsOfService" : "https://example.com",
"contact" : {
"name" : "Homer Simpson",
"url" : "https://gph.is/1NPUDiM",
"email" : "chunkylover53@aol.com"
},
"license" : {
"name" : "MIT",
"url" : "https://github.com/bkbnio/kompendium/blob/main/LICENSE"
}
},
"servers" : [ {
"url" : "https://myawesomeapi.com",
"description" : "Production instance of my API"
}, {
"url" : "https://staging.myawesomeapi.com",
"description" : "Where the fun stuff happens"
} ],
"paths" : {
"/test/polymorphiclist" : {
"get" : {
"tags" : [ ],
"summary" : "Oh so many gibbits",
"description" : "Polymorphic list response",
"parameters" : [ ],
"responses" : {
"200" : {
"description" : "A successful endeavor",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/List-FlibbityGibbit"
}
}
}
}
},
"deprecated" : false
}
}
},
"components" : {
"schemas" : {
"String" : {
"type" : "string"
},
"SimpleGibbit" : {
"type" : "object",
"properties" : {
"a" : {
"$ref" : "#/components/schemas/String"
}
}
},
"Int" : {
"type" : "integer",
"format" : "int32"
},
"ComplexGibbit" : {
"type" : "object",
"properties" : {
"b" : {
"$ref" : "#/components/schemas/String"
},
"c" : {
"$ref" : "#/components/schemas/Int"
}
}
},
"List-FlibbityGibbit" : {
"type" : "array",
"items" : {
"anyOf" : [ {
"$ref" : "#/components/schemas/SimpleGibbit"
}, {
"$ref" : "#/components/schemas/ComplexGibbit"
} ]
}
}
},
"securitySchemes" : { }
},
"security" : [ ],
"tags" : [ ]
}

View File

@ -0,0 +1,91 @@
{
"openapi" : "3.0.3",
"info" : {
"title" : "Test API",
"version" : "1.33.7",
"description" : "An amazing, fully-ish 😉 generated API spec",
"termsOfService" : "https://example.com",
"contact" : {
"name" : "Homer Simpson",
"url" : "https://gph.is/1NPUDiM",
"email" : "chunkylover53@aol.com"
},
"license" : {
"name" : "MIT",
"url" : "https://github.com/bkbnio/kompendium/blob/main/LICENSE"
}
},
"servers" : [ {
"url" : "https://myawesomeapi.com",
"description" : "Production instance of my API"
}, {
"url" : "https://staging.myawesomeapi.com",
"description" : "Where the fun stuff happens"
} ],
"paths" : {
"/test/polymorphicmap" : {
"get" : {
"tags" : [ ],
"summary" : "By gawd that's a lot of gibbits",
"description" : "Polymorphic list response",
"parameters" : [ ],
"responses" : {
"200" : {
"description" : "A successful endeavor",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/Map-String-FlibbityGibbit"
}
}
}
}
},
"deprecated" : false
}
}
},
"components" : {
"schemas" : {
"String" : {
"type" : "string"
},
"SimpleGibbit" : {
"type" : "object",
"properties" : {
"a" : {
"$ref" : "#/components/schemas/String"
}
}
},
"Int" : {
"type" : "integer",
"format" : "int32"
},
"ComplexGibbit" : {
"type" : "object",
"properties" : {
"b" : {
"$ref" : "#/components/schemas/String"
},
"c" : {
"$ref" : "#/components/schemas/Int"
}
}
},
"Map-String-FlibbityGibbit" : {
"type" : "object",
"additionalProperties" : {
"anyOf" : [ {
"$ref" : "#/components/schemas/SimpleGibbit"
}, {
"$ref" : "#/components/schemas/ComplexGibbit"
} ]
}
}
},
"securitySchemes" : { }
},
"security" : [ ],
"tags" : [ ]
}

View File

@ -0,0 +1,83 @@
{
"openapi" : "3.0.3",
"info" : {
"title" : "Test API",
"version" : "1.33.7",
"description" : "An amazing, fully-ish 😉 generated API spec",
"termsOfService" : "https://example.com",
"contact" : {
"name" : "Homer Simpson",
"url" : "https://gph.is/1NPUDiM",
"email" : "chunkylover53@aol.com"
},
"license" : {
"name" : "MIT",
"url" : "https://github.com/bkbnio/kompendium/blob/main/LICENSE"
}
},
"servers" : [ {
"url" : "https://myawesomeapi.com",
"description" : "Production instance of my API"
}, {
"url" : "https://staging.myawesomeapi.com",
"description" : "Where the fun stuff happens"
} ],
"paths" : {
"/test/polymorphicmap" : {
"get" : {
"tags" : [ ],
"summary" : "Come on and slam",
"description" : "and welcome to the jam",
"parameters" : [ ],
"responses" : {
"200" : {
"description" : "A successful endeavor",
"content" : {
"application/json" : {
"schema" : {
"anyOf" : [ {
"$ref" : "#/components/schemas/OneJamma"
}, {
"$ref" : "#/components/schemas/AnothaJamma"
} ]
}
}
}
}
},
"deprecated" : false
}
}
},
"components" : {
"schemas" : {
"Int" : {
"type" : "integer",
"format" : "int32"
},
"OneJamma" : {
"type" : "object",
"properties" : {
"a" : {
"$ref" : "#/components/schemas/Int"
}
}
},
"Float" : {
"type" : "number",
"format" : "float"
},
"AnothaJamma" : {
"type" : "object",
"properties" : {
"b" : {
"$ref" : "#/components/schemas/Float"
}
}
}
},
"securitySchemes" : { }
},
"security" : [ ],
"tags" : [ ]
}

View File

@ -0,0 +1,73 @@
{
"openapi" : "3.0.3",
"info" : {
"title" : "Test API",
"version" : "1.33.7",
"description" : "An amazing, fully-ish 😉 generated API spec",
"termsOfService" : "https://example.com",
"contact" : {
"name" : "Homer Simpson",
"url" : "https://gph.is/1NPUDiM",
"email" : "chunkylover53@aol.com"
},
"license" : {
"name" : "MIT",
"url" : "https://github.com/bkbnio/kompendium/blob/main/LICENSE"
}
},
"servers" : [ {
"url" : "https://myawesomeapi.com",
"description" : "Production instance of my API"
}, {
"url" : "https://staging.myawesomeapi.com",
"description" : "Where the fun stuff happens"
} ],
"paths" : {
"/test/polymorphic" : {
"get" : {
"tags" : [ ],
"summary" : "spooky class",
"description" : "break this glass in scenario of emergency",
"parameters" : [ ],
"responses" : {
"200" : {
"description" : "A successful endeavor",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/Mysterious"
}
}
}
}
},
"deprecated" : false
}
}
},
"components" : {
"schemas" : {
"String" : {
"type" : "string"
},
"Hehe" : {
"type" : "string",
"enum" : [ "HAHA", "HOHO" ]
},
"Mysterious" : {
"type" : "object",
"properties" : {
"nowYouSeeMe" : {
"$ref" : "#/components/schemas/String"
},
"nowYouDont" : {
"$ref" : "#/components/schemas/Hehe"
}
}
}
},
"securitySchemes" : { }
},
"security" : [ ],
"tags" : [ ]
}

View File

@ -18,6 +18,8 @@ dependencies {
implementation(libs.bundles.ktorAuth) implementation(libs.bundles.ktorAuth)
implementation(libs.bundles.logging) implementation(libs.bundles.logging)
implementation("joda-time:joda-time:2.10.10")
testImplementation("org.jetbrains.kotlin:kotlin-test") testImplementation("org.jetbrains.kotlin:kotlin-test")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit") testImplementation("org.jetbrains.kotlin:kotlin-test-junit")
} }

View File

@ -1,5 +1,6 @@
package io.bkbn.kompendium.playground package io.bkbn.kompendium.playground
import io.bkbn.kompendium.Kompendium
import io.bkbn.kompendium.Notarized.notarizedDelete import io.bkbn.kompendium.Notarized.notarizedDelete
import io.bkbn.kompendium.Notarized.notarizedException import io.bkbn.kompendium.Notarized.notarizedException
import io.bkbn.kompendium.Notarized.notarizedGet import io.bkbn.kompendium.Notarized.notarizedGet
@ -7,7 +8,9 @@ import io.bkbn.kompendium.Notarized.notarizedPost
import io.bkbn.kompendium.Notarized.notarizedPut import io.bkbn.kompendium.Notarized.notarizedPut
import io.bkbn.kompendium.auth.KompendiumAuth.notarizedBasic import io.bkbn.kompendium.auth.KompendiumAuth.notarizedBasic
import io.bkbn.kompendium.models.meta.ResponseInfo import io.bkbn.kompendium.models.meta.ResponseInfo
import io.bkbn.kompendium.models.oas.FormatSchema
import io.bkbn.kompendium.playground.PlaygroundToC.testAuthenticatedSingleGetInfo import io.bkbn.kompendium.playground.PlaygroundToC.testAuthenticatedSingleGetInfo
import io.bkbn.kompendium.playground.PlaygroundToC.testCustomOverride
import io.bkbn.kompendium.playground.PlaygroundToC.testGetWithExamples import io.bkbn.kompendium.playground.PlaygroundToC.testGetWithExamples
import io.bkbn.kompendium.playground.PlaygroundToC.testIdGetInfo import io.bkbn.kompendium.playground.PlaygroundToC.testIdGetInfo
import io.bkbn.kompendium.playground.PlaygroundToC.testPostWithExamples import io.bkbn.kompendium.playground.PlaygroundToC.testPostWithExamples
@ -16,6 +19,7 @@ import io.bkbn.kompendium.playground.PlaygroundToC.testSingleGetInfo
import io.bkbn.kompendium.playground.PlaygroundToC.testSingleGetInfoWithThrowable import io.bkbn.kompendium.playground.PlaygroundToC.testSingleGetInfoWithThrowable
import io.bkbn.kompendium.playground.PlaygroundToC.testSinglePostInfo import io.bkbn.kompendium.playground.PlaygroundToC.testSinglePostInfo
import io.bkbn.kompendium.playground.PlaygroundToC.testSinglePutInfo import io.bkbn.kompendium.playground.PlaygroundToC.testSinglePutInfo
import io.bkbn.kompendium.playground.PlaygroundToC.testUndeclaredFields
import io.bkbn.kompendium.routes.openApi import io.bkbn.kompendium.routes.openApi
import io.bkbn.kompendium.routes.redoc import io.bkbn.kompendium.routes.redoc
import io.bkbn.kompendium.swagger.swaggerUI import io.bkbn.kompendium.swagger.swaggerUI
@ -36,8 +40,11 @@ import io.ktor.serialization.json
import io.ktor.server.engine.embeddedServer import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty import io.ktor.server.netty.Netty
import io.ktor.webjars.Webjars import io.ktor.webjars.Webjars
import org.joda.time.DateTime
fun main() { fun main() {
Kompendium.addCustomTypeSchema(DateTime::class, FormatSchema("date-time", "string"))
embeddedServer( embeddedServer(
Netty, Netty,
port = 8081, port = 8081,
@ -114,6 +121,11 @@ fun Application.mainModule() {
call.respondText { "heya" } call.respondText { "heya" }
} }
} }
route("custom_override") {
notarizedGet(testCustomOverride) {
call.respondText { DateTime.now().toString() }
}
}
authenticate("basic") { authenticate("basic") {
route("/authenticated/single") { route("/authenticated/single") {
notarizedGet(testAuthenticatedSingleGetInfo) { notarizedGet(testAuthenticatedSingleGetInfo) {
@ -127,5 +139,10 @@ fun Application.mainModule() {
error("bad things just happened") error("bad things just happened")
} }
} }
route("/undeclared") {
notarizedGet(testUndeclaredFields) {
call.respondText { "hi" }
}
}
} }
} }

View File

@ -3,6 +3,9 @@ package io.bkbn.kompendium.playground
import io.bkbn.kompendium.annotations.KompendiumField import io.bkbn.kompendium.annotations.KompendiumField
import io.bkbn.kompendium.annotations.KompendiumParam import io.bkbn.kompendium.annotations.KompendiumParam
import io.bkbn.kompendium.annotations.ParamType import io.bkbn.kompendium.annotations.ParamType
import io.bkbn.kompendium.annotations.UndeclaredField
import kotlinx.serialization.Serializable
import org.joda.time.DateTime
data class ExampleParams( data class ExampleParams(
@KompendiumParam(ParamType.PATH) val id: Int, @KompendiumParam(ParamType.PATH) val id: Int,
@ -25,8 +28,19 @@ data class ExampleRequest(
val aaa: List<Long> val aaa: List<Long>
) )
@Serializable
data class ExampleResponse(val c: String) data class ExampleResponse(val c: String)
data class ExceptionResponse(val message: String) data class ExceptionResponse(val message: String)
data class ExampleCreatedResponse(val id: Int, val c: String) data class ExampleCreatedResponse(val id: Int, val c: String)
data class DateTimeWrapper(val dt: DateTime)
enum class Testerino {
First,
Second
}
@UndeclaredField("type", Testerino::class)
data class SimpleYetMysterious(val exists: Boolean)

View File

@ -55,6 +55,15 @@ object PlaygroundToC {
description = "Returns a different sample" description = "Returns a different sample"
) )
) )
val testCustomOverride = MethodInfo.GetInfo<Unit, DateTimeWrapper>(
summary = "custom schema test",
description = "testing",
tags = setOf("custom"),
responseInfo = ResponseInfo(
status = HttpStatusCode.OK,
description = "good tings"
)
)
val testSingleGetInfoWithThrowable = testSingleGetInfo.copy( val testSingleGetInfoWithThrowable = testSingleGetInfo.copy(
summary = "Show me the error baby 🙏", summary = "Show me the error baby 🙏",
canThrow = setOf(Exception::class) canThrow = setOf(Exception::class)
@ -100,4 +109,13 @@ object PlaygroundToC {
), ),
securitySchemes = setOf("basic") securitySchemes = setOf("basic")
) )
val testUndeclaredFields = MethodInfo.GetInfo<Unit, SimpleYetMysterious>(
summary = "Tests adding undeclared fields",
description = "vvv mysterious",
tags = setOf("mysterious"),
responseInfo = ResponseInfo(
status = HttpStatusCode.OK,
description = "good tings"
)
)
} }

View File

@ -1,4 +1,5 @@
rootProject.name = "kompendium" rootProject.name = "kompendium"
include("kompendium-core") include("kompendium-core")
include("kompendium-auth") include("kompendium-auth")
include("kompendium-swagger-ui") include("kompendium-swagger-ui")