V1 beta release time 🥳 (#48)

This commit is contained in:
Ryan Brink
2021-05-06 09:36:35 -04:00
committed by GitHub
parent f23016e878
commit c019f92cc0
22 changed files with 616 additions and 465 deletions

25
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,25 @@
name: Publish to GitHub Packages
on:
release:
types:
- prereleased
- released
jobs:
publish:
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 package
run: ./gradlew publish -Prelease=true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -1,5 +1,17 @@
# Changelog # Changelog
### [1.0.0-beta] - May 6th, 2021
### Added
- Release action to package a release JAR 🍻
- EXTREME DOCUMENTATION 📜
### Changed
- Cleanup to test files
- Removes KompendiumHttpCodes in favor of Ktor HttpStatusCode
### [0.9.0] - May 5th, 2021 ### [0.9.0] - May 5th, 2021
### Added ### Added

View File

@ -37,16 +37,6 @@ dependencies {
## In depth ## In depth
### Warning 🚨
Kompendium is still under active development ⚠️ There are a number of yet-to-be-implemented features, including
- Sealed Class / Polymorphic Support 😬
- Validation / Enforcement (❓👀❓)
If you have a feature that is not listed here, please open an issue!
In addition, if you find any 🐞😱 please open an issue as well!
### Notarized Routes ### Notarized Routes
Kompendium introduces the concept of `notarized` HTTP methods. That is, for all your `GET`, `POST`, `PUT`, and `DELETE` Kompendium introduces the concept of `notarized` HTTP methods. That is, for all your `GET`, `POST`, `PUT`, and `DELETE`
@ -204,3 +194,17 @@ it offers a seriously clean UX where the implementer doesn't need to worry about
drawback, however, is that you are limited to a single API per classpath. drawback, however, is that you are limited to a single API per classpath.
If this is a blocker, please open a GitHub issue, and we can start to think out solutions! If this is a blocker, please open a GitHub issue, and we can start to think out solutions!
## 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/lg-backbone/kompendium/milestone/2). Among others, this includes
- Polymorphic support
- 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/lg-backbone/kompendium/issues/new)

View File

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

View File

@ -11,6 +11,7 @@ import io.ktor.auth.authenticate
import io.ktor.features.ContentNegotiation import io.ktor.features.ContentNegotiation
import io.ktor.http.ContentType import io.ktor.http.ContentType
import io.ktor.http.HttpMethod import io.ktor.http.HttpMethod
import io.ktor.http.HttpStatusCode
import io.ktor.jackson.jackson import io.ktor.jackson.jackson
import io.ktor.response.respondText import io.ktor.response.respondText
import io.ktor.routing.route import io.ktor.routing.route
@ -31,7 +32,6 @@ import org.leafygreens.kompendium.models.meta.MethodInfo
import org.leafygreens.kompendium.models.meta.ResponseInfo import org.leafygreens.kompendium.models.meta.ResponseInfo
import org.leafygreens.kompendium.routes.openApi import org.leafygreens.kompendium.routes.openApi
import org.leafygreens.kompendium.routes.redoc import org.leafygreens.kompendium.routes.redoc
import org.leafygreens.kompendium.util.KompendiumHttpCodes
internal class KompendiumAuthTest { internal class KompendiumAuthTest {
@ -189,7 +189,7 @@ internal class KompendiumAuthTest {
} }
private companion object { private companion object {
val testGetResponse = ResponseInfo<TestResponse>(KompendiumHttpCodes.OK, "A Successful Endeavor") val testGetResponse = ResponseInfo<TestResponse>(HttpStatusCode.OK, "A Successful Endeavor")
fun testGetInfo(vararg security: String) = fun testGetInfo(vararg security: String) =
MethodInfo.GetInfo<TestParams, TestResponse>( MethodInfo.GetInfo<TestParams, TestResponse>(
summary = "Another get test", summary = "Another get test",

View File

@ -7,6 +7,9 @@ import org.leafygreens.kompendium.models.oas.OpenApiSpecInfo
import org.leafygreens.kompendium.path.CorePathCalculator import org.leafygreens.kompendium.path.CorePathCalculator
import org.leafygreens.kompendium.path.PathCalculator import org.leafygreens.kompendium.path.PathCalculator
/**
* Maintains all state for the Kompendium library
*/
object Kompendium { object Kompendium {
var errorMap: ErrorMap = emptyMap() var errorMap: ErrorMap = emptyMap()

View File

@ -4,8 +4,19 @@ import io.ktor.routing.Route
import kotlin.reflect.KType import kotlin.reflect.KType
import kotlin.reflect.typeOf import kotlin.reflect.typeOf
/**
* Functions are considered preflight when they are used to intercept a method ahead of running.
*/
object KompendiumPreFlight { 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) @OptIn(ExperimentalStdlibApi::class)
inline fun <reified TParam : Any, reified TReq : Any, reified TResp : Any> methodNotarizationPreFlight( inline fun <reified TParam : Any, reified TReq : Any, reified TResp : Any> methodNotarizationPreFlight(
block: (KType, KType, KType) -> Route block: (KType, KType, KType) -> Route
@ -20,6 +31,12 @@ object KompendiumPreFlight {
return block.invoke(paramType, requestType, responseType) 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) @OptIn(ExperimentalStdlibApi::class)
inline fun <reified TErr: Throwable, reified TResp : Any> errorNotarizationPreFlight( inline fun <reified TErr: Throwable, reified TResp : Any> errorNotarizationPreFlight(
block: (KType, KType) -> Unit block: (KType, KType) -> Unit

View File

@ -21,10 +21,19 @@ import org.leafygreens.kompendium.util.Helpers.getReferenceSlug
import org.leafygreens.kompendium.util.Helpers.logged import org.leafygreens.kompendium.util.Helpers.logged
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
/**
* Responsible for generating the schema map that is used to power all object references across the API Spec.
*/
object Kontent { object Kontent {
private val logger = LoggerFactory.getLogger(javaClass) 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) @OptIn(ExperimentalStdlibApi::class)
inline fun <reified T> generateKontent( inline fun <reified T> generateKontent(
cache: SchemaMap = emptyMap() cache: SchemaMap = emptyMap()
@ -33,6 +42,12 @@ object Kontent {
return generateKTypeKontent(kontentType, cache) return generateKTypeKontent(kontentType, 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) @OptIn(ExperimentalStdlibApi::class)
inline fun <reified T> generateParameterKontent( inline fun <reified T> generateParameterKontent(
cache: SchemaMap = emptyMap() cache: SchemaMap = emptyMap()
@ -42,6 +57,11 @@ object Kontent {
.filterNot { (slug, _) -> slug == (kontentType.classifier as KClass<*>).simpleName } .filterNot { (slug, _) -> slug == (kontentType.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( fun generateKTypeKontent(
type: KType, type: KType,
cache: SchemaMap = emptyMap() cache: SchemaMap = emptyMap()
@ -65,6 +85,11 @@ object Kontent {
} }
} }
/**
* 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(clazz: KClass<*>, cache: SchemaMap): SchemaMap = private fun handleComplexType(clazz: KClass<*>, cache: SchemaMap): SchemaMap =
when (cache.containsKey(clazz.simpleName)) { when (cache.containsKey(clazz.simpleName)) {
true -> { true -> {
@ -92,11 +117,22 @@ object Kontent {
} }
} }
/**
* 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 { private fun handleEnumType(clazz: KClass<*>, cache: SchemaMap): SchemaMap {
val options = clazz.java.enumConstants.map { it.toString() }.toSet() val options = clazz.java.enumConstants.map { it.toString() }.toSet()
return cache.plus(clazz.simpleName!! to EnumSchema(options)) 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 { private fun handleMapType(type: KType, clazz: KClass<*>, cache: SchemaMap): SchemaMap {
logger.debug("Map detected for $type, generating schema and appending to cache") logger.debug("Map detected for $type, generating schema and appending to cache")
val (keyType, valType) = type.arguments.map { it.type } val (keyType, valType) = type.arguments.map { it.type }
@ -112,6 +148,12 @@ object Kontent {
return updatedCache.plus(referenceName to schema) 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 { private fun handleCollectionType(type: KType, clazz: KClass<*>, cache: SchemaMap): SchemaMap {
logger.debug("Collection detected for $type, generating schema and appending to cache") logger.debug("Collection detected for $type, generating schema and appending to cache")
val collectionType = type.arguments.first().type!! val collectionType = type.arguments.first().type!!

View File

@ -26,7 +26,19 @@ import org.leafygreens.kompendium.util.Helpers
import org.leafygreens.kompendium.util.Helpers.getReferenceSlug import org.leafygreens.kompendium.util.Helpers.getReferenceSlug
import org.leafygreens.kompendium.util.Helpers.getSimpleSlug import org.leafygreens.kompendium.util.Helpers.getSimpleSlug
/**
* The MethodParser is responsible for converting route metadata and types into an OpenAPI compatible data class.
*/
object MethodParser { 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( fun parseMethodInfo(
info: MethodInfo<*, *>, info: MethodInfo<*, *>,
paramType: KType, paramType: KType,
@ -61,19 +73,34 @@ object MethodParser {
) else null ) else null
) )
private fun parseThrowables(throwables: Set<KClass<*>>): Map<Int, OpenApiSpecReferencable> = throwables.mapNotNull { /**
Kompendium.errorMap[it.createType()] * Adds the error to the [Kompendium.errorMap] for reference in notarized routes.
}.toMap() * @param errorType [KType] of the throwable being handled
* @param responseType [KType] the type of the response sent in event of error
fun <TResp> ResponseInfo<TResp>.parseErrorInfo( */
fun ResponseInfo<*>.parseErrorInfo(
errorType: KType, errorType: KType,
responseType: KType responseType: KType
) { ) {
Kompendium.errorMap = Kompendium.errorMap.plus(errorType to responseType.toResponseSpec(this)) Kompendium.errorMap = Kompendium.errorMap.plus(errorType to responseType.toResponseSpec(this))
} }
// TODO These two lookin' real similar 👀 Combine? /**
private fun <TReq> KType.toRequestSpec(requestInfo: RequestInfo<TReq>?): OpenApiSpecRequest<TReq>? = * 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) { when (requestInfo) {
null -> null null -> null
else -> { else -> {
@ -84,18 +111,31 @@ object MethodParser {
} }
} }
private fun <TResp> KType.toResponseSpec(responseInfo: ResponseInfo<TResp>?): Pair<Int, OpenApiSpecResponse<TResp>>? = /**
* 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) { when (responseInfo) {
null -> null // TODO again probably revisit this null -> null
else -> { else -> {
val specResponse = OpenApiSpecResponse( val specResponse = OpenApiSpecResponse(
description = responseInfo.description, description = responseInfo.description,
content = resolveContent(responseInfo.mediaTypes, responseInfo.examples) content = resolveContent(responseInfo.mediaTypes, responseInfo.examples)
) )
Pair(responseInfo.status, specResponse) Pair(responseInfo.status.value, specResponse)
} }
} }
/**
* Generates MediaTypes along with any examples provided
* @receiver [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> KType.resolveContent( private fun <F> KType.resolveContent(
mediaTypes: List<String>, mediaTypes: List<String>,
examples: Map<String, F> examples: Map<String, F>
@ -111,6 +151,13 @@ object MethodParser {
} else null } else null
} }
/**
* Parses a type for all parameter information. All fields in the receiver
* must be annotated with [org.leafygreens.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> { private fun KType.toParameterSpec(): List<OpenApiSpecParameter> {
val clazz = classifier as KClass<*> val clazz = classifier as KClass<*>
return clazz.memberProperties.map { prop -> return clazz.memberProperties.map { prop ->
@ -131,6 +178,12 @@ object MethodParser {
} }
} }
/**
* 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? { private fun getDefaultParameterValue(clazz: KClass<*>, prop: KProperty<*>): Any? {
val constructor = clazz.primaryConstructor val constructor = clazz.primaryConstructor
val parameterInQuestion = constructor val parameterInQuestion = constructor
@ -152,6 +205,12 @@ object MethodParser {
return getterFunction.invoke(instance) 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) { private fun defaultValueInjector(param: KParameter): Any = when (param.type.classifier) {
String::class -> "test" String::class -> "test"
Boolean::class -> false Boolean::class -> false
@ -162,5 +221,4 @@ object MethodParser {
UUID::class -> UUID.randomUUID() UUID::class -> UUID.randomUUID()
else -> error("Unsupported Type") else -> error("Unsupported Type")
} }
} }

View File

@ -17,9 +17,20 @@ import org.leafygreens.kompendium.models.meta.MethodInfo.DeleteInfo
import org.leafygreens.kompendium.models.meta.ResponseInfo import org.leafygreens.kompendium.models.meta.ResponseInfo
import org.leafygreens.kompendium.models.oas.OpenApiSpecPathItem import org.leafygreens.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 { object Notarized {
@OptIn(ExperimentalStdlibApi::class) /**
* Notarization for an HTTP GET request
* @param TParam The class containing all parameter fields.
* Each field must be annotated with @[org.leafygreens.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( inline fun <reified TParam : Any, reified TResp : Any> Route.notarizedGet(
info: GetInfo<TParam, TResp>, info: GetInfo<TParam, TResp>,
noinline body: PipelineInterceptor<Unit, ApplicationCall> noinline body: PipelineInterceptor<Unit, ApplicationCall>
@ -31,6 +42,14 @@ object Notarized {
return method(HttpMethod.Get) { handle(body) } 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 @[org.leafygreens.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( inline fun <reified TParam : Any, reified TReq : Any, reified TResp : Any> Route.notarizedPost(
info: PostInfo<TParam, TReq, TResp>, info: PostInfo<TParam, TReq, TResp>,
noinline body: PipelineInterceptor<Unit, ApplicationCall> noinline body: PipelineInterceptor<Unit, ApplicationCall>
@ -42,6 +61,14 @@ object Notarized {
return method(HttpMethod.Post) { handle(body) } 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 @[org.leafygreens.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( inline fun <reified TParam : Any, reified TReq : Any, reified TResp : Any> Route.notarizedPut(
info: PutInfo<TParam, TReq, TResp>, info: PutInfo<TParam, TReq, TResp>,
noinline body: PipelineInterceptor<Unit, ApplicationCall>, noinline body: PipelineInterceptor<Unit, ApplicationCall>,
@ -54,6 +81,13 @@ object Notarized {
return method(HttpMethod.Put) { handle(body) } 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 @[org.leafygreens.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( inline fun <reified TParam : Any, reified TResp : Any> Route.notarizedDelete(
info: DeleteInfo<TParam, TResp>, info: DeleteInfo<TParam, TResp>,
noinline body: PipelineInterceptor<Unit, ApplicationCall> noinline body: PipelineInterceptor<Unit, ApplicationCall>
@ -65,6 +99,12 @@ object Notarized {
return method(HttpMethod.Delete) { handle(body) } 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( inline fun <reified TErr : Throwable, reified TResp : Any> StatusPages.Configuration.notarizedException(
info: ResponseInfo<TResp>, info: ResponseInfo<TResp>,
noinline handler: suspend PipelineContext<Unit, ApplicationCall>.(TErr) -> Unit noinline handler: suspend PipelineContext<Unit, ApplicationCall>.(TErr) -> Unit

View File

@ -1,7 +1,9 @@
package org.leafygreens.kompendium.models.meta package org.leafygreens.kompendium.models.meta
import io.ktor.http.HttpStatusCode
data class ResponseInfo<TResp>( data class ResponseInfo<TResp>(
val status: Int, val status: HttpStatusCode,
val description: String, val description: String,
val mediaTypes: List<String> = listOf("application/json"), val mediaTypes: List<String> = listOf("application/json"),
val examples: Map<String, TResp> = emptyMap() val examples: Map<String, TResp> = emptyMap()

View File

@ -7,6 +7,9 @@ import io.ktor.routing.Route
import io.ktor.util.InternalAPI import io.ktor.util.InternalAPI
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
/**
* Default [PathCalculator] meant to be overridden as necessary
*/
open class CorePathCalculator : PathCalculator { open class CorePathCalculator : PathCalculator {
private val logger = LoggerFactory.getLogger(javaClass) private val logger = LoggerFactory.getLogger(javaClass)

View File

@ -2,10 +2,19 @@ package org.leafygreens.kompendium.path
import io.ktor.routing.Route import io.ktor.routing.Route
/**
* Extensible interface for calculating Ktor paths
*/
interface PathCalculator { interface PathCalculator {
/**
* Core route calculation method
*/
fun calculate(route: Route?, tail: String = ""): String fun calculate(route: Route?, tail: String = ""): String
/**
* Used to handle any custom selectors that may be missed by the base route calculation
*/
fun handleCustomSelectors(route: Route, tail: String): String fun handleCustomSelectors(route: Route, tail: String): String
} }

View File

@ -7,6 +7,10 @@ import io.ktor.routing.get
import io.ktor.routing.route import io.ktor.routing.route
import org.leafygreens.kompendium.models.oas.OpenApiSpec import org.leafygreens.kompendium.models.oas.OpenApiSpec
/**
* Provides an out-of-the-box route to return the generated [OpenApiSpec]
* @param oas spec that is returned
*/
fun Routing.openApi(oas: OpenApiSpec) { fun Routing.openApi(oas: OpenApiSpec) {
route("/openapi.json") { route("/openapi.json") {
get { get {

View File

@ -15,7 +15,12 @@ import kotlinx.html.title
import kotlinx.html.unsafe import kotlinx.html.unsafe
import org.leafygreens.kompendium.models.oas.OpenApiSpec import org.leafygreens.kompendium.models.oas.OpenApiSpec
fun Routing.redoc(oas: OpenApiSpec) { /**
* Provides an out-of-the-box route to view docs using ReDoc
* @param oas spec to reference
* @param specUrl url to point ReDoc to the OpenAPI json document
*/
fun Routing.redoc(oas: OpenApiSpec, specUrl: String = "/openapi.json") {
route("/docs") { route("/docs") {
get { get {
call.respondHtml { call.respondHtml {
@ -41,8 +46,7 @@ fun Routing.redoc(oas: OpenApiSpec) {
} }
} }
body { body {
// TODO needs to mirror openApi route unsafe { +"<redoc spec-url='${specUrl}'></redoc>" }
unsafe { +"<redoc spec-url='/openapi.json'></redoc>" }
script { script {
src = "https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js" src = "https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js"
} }

View File

@ -1,81 +0,0 @@
package org.leafygreens.kompendium.util
// Take from https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
object KompendiumHttpCodes {
// Informational responses
const val CONTINUE = 100
const val SWITCHING_PROTOCOL = 101
const val PROCESSING = 102
const val EARLY_HINTS = 103
// Successful responses
const val OK = 200
const val CREATED = 201
const val ACCEPTED = 202
const val NON_AUTHORITATIVE_INFORMATION = 203
const val NO_CONTENT = 204
const val RESET_CONTENT = 205
const val PARTIAL_CONTENT = 206
const val MULTI_STATUS = 207
const val ALREADY_REPORTED = 208
const val IM_USED = 226
// Redirection messages
const val MULTIPLE_CHOICE = 300
const val MOVED_PERMANENTLY = 301
const val FOUND = 302
const val SEE_OTHER = 303
const val NOT_MODIFIED = 304
@Deprecated("Deprecated due to security concerns regarding in-band configuration of a proxy")
const val USE_PROXY = 305
@Deprecated("This response code is no longer used; it is just reserved.")
const val UNUSED = 306
const val TEMPORARY_REDIRECT = 307
const val PERMANENT_REDIRECT = 308
// Client Response Errors
const val BAD_REQUEST = 400
const val UNAUTHORIZED = 401
const val PAYMENT_REQUIRED = 402
const val FORBIDDEN = 403
const val NOT_FOUND = 404
const val METHOD_NOT_ALLOWED = 405
const val NOT_ACCEPTABLE = 406
const val PROXY_AUTHENTICATION_REQUIRED = 407
const val REQUEST_TIMEOUT = 408
const val CONFLICT = 409
const val GONE = 410
const val LENGTH_REQUIRED = 411
const val PRECONDITION_FAILED = 412
const val PAYLOAD_TOO_LARGE = 413
const val URI_TOO_LONG = 414
const val UNSUPPORTED_MEDIA_TYPE = 415
const val RANGE_NOT_SATISFIABLE = 416
const val EXPECTATION_FAILED = 417
const val IM_A_TEAPOT = 418
const val MISDIRECTED_REQUEST = 421
const val UNPROCESSABLE_ENTITY = 422
const val LOCKED = 423
const val FAILED_DEPENDENCY = 424
const val TOO_EARLY = 425
const val UPGRADE_REQUIRED = 426
const val PRECONDITION_REQUIRED = 428
const val TOO_MANY_REQUESTS = 429
const val REQUEST_HEADER_FIELDS_TOO_LARGE = 431
const val UNAVAILABLE_FOR_LEGAL_REASONS = 451
// Server Error Responses
const val INTERNAL_SERVER_ERROR = 500
const val NOT_IMPLEMENTED = 501
const val BAD_GATEWAY = 502
const val SERVICE_UNAVAILABLE = 503
const val GATEWAY_TIMEOUT = 504
const val HTTP_VERSION_NOT_SUPPORTED = 505
const val VARIANT_ALSO_NEGOTIATES = 506
const val INSUFFICIENT_STORAGE = 507
const val LOOP_DETECTED = 508
const val NOT_EXTENDED = 510
const val NETWORK_AUTHENTICATION_REQUIRED = 511
}

View File

@ -1,18 +1,8 @@
package org.leafygreens.kompendium package org.leafygreens.kompendium
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.databind.SerializationFeature
import io.ktor.application.Application import io.ktor.application.Application
import io.ktor.application.call
import io.ktor.application.install
import io.ktor.features.ContentNegotiation
import io.ktor.features.StatusPages
import io.ktor.http.HttpMethod import io.ktor.http.HttpMethod
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.jackson.jackson
import io.ktor.response.respond
import io.ktor.response.respondText
import io.ktor.routing.route
import io.ktor.routing.routing import io.ktor.routing.routing
import io.ktor.server.testing.handleRequest import io.ktor.server.testing.handleRequest
import io.ktor.server.testing.withTestApplication import io.ktor.server.testing.withTestApplication
@ -20,35 +10,33 @@ import java.net.URI
import kotlin.test.AfterTest import kotlin.test.AfterTest
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import org.leafygreens.kompendium.Notarized.notarizedDelete
import org.leafygreens.kompendium.Notarized.notarizedException
import org.leafygreens.kompendium.Notarized.notarizedGet
import org.leafygreens.kompendium.Notarized.notarizedPost
import org.leafygreens.kompendium.Notarized.notarizedPut
import org.leafygreens.kompendium.annotations.KompendiumParam
import org.leafygreens.kompendium.annotations.ParamType
import org.leafygreens.kompendium.models.meta.MethodInfo.DeleteInfo
import org.leafygreens.kompendium.models.meta.MethodInfo.GetInfo
import org.leafygreens.kompendium.models.meta.MethodInfo.PostInfo
import org.leafygreens.kompendium.models.meta.MethodInfo.PutInfo
import org.leafygreens.kompendium.models.meta.RequestInfo
import org.leafygreens.kompendium.models.meta.ResponseInfo
import org.leafygreens.kompendium.models.oas.OpenApiSpecInfo import org.leafygreens.kompendium.models.oas.OpenApiSpecInfo
import org.leafygreens.kompendium.models.oas.OpenApiSpecInfoContact import org.leafygreens.kompendium.models.oas.OpenApiSpecInfoContact
import org.leafygreens.kompendium.models.oas.OpenApiSpecInfoLicense import org.leafygreens.kompendium.models.oas.OpenApiSpecInfoLicense
import org.leafygreens.kompendium.models.oas.OpenApiSpecServer import org.leafygreens.kompendium.models.oas.OpenApiSpecServer
import org.leafygreens.kompendium.routes.openApi import org.leafygreens.kompendium.routes.openApi
import org.leafygreens.kompendium.routes.redoc import org.leafygreens.kompendium.routes.redoc
import org.leafygreens.kompendium.util.ComplexRequest
import org.leafygreens.kompendium.util.DefaultParameter
import org.leafygreens.kompendium.util.ExceptionResponse
import org.leafygreens.kompendium.util.KompendiumHttpCodes
import org.leafygreens.kompendium.util.TestCreatedResponse
import org.leafygreens.kompendium.util.TestHelpers.getFileSnapshot import org.leafygreens.kompendium.util.TestHelpers.getFileSnapshot
import org.leafygreens.kompendium.util.TestNested import org.leafygreens.kompendium.util.complexType
import org.leafygreens.kompendium.util.TestParams import org.leafygreens.kompendium.util.configModule
import org.leafygreens.kompendium.util.TestRequest import org.leafygreens.kompendium.util.emptyGet
import org.leafygreens.kompendium.util.TestResponse import org.leafygreens.kompendium.util.nestedUnderRootModule
import org.leafygreens.kompendium.util.nonRequiredParamsGet
import org.leafygreens.kompendium.util.notarizedDeleteModule
import org.leafygreens.kompendium.util.notarizedGetModule
import org.leafygreens.kompendium.util.notarizedGetWithMultipleThrowables
import org.leafygreens.kompendium.util.notarizedGetWithNotarizedException
import org.leafygreens.kompendium.util.notarizedPostModule
import org.leafygreens.kompendium.util.notarizedPutModule
import org.leafygreens.kompendium.util.pathParsingTestModule
import org.leafygreens.kompendium.util.primitives
import org.leafygreens.kompendium.util.returnsList
import org.leafygreens.kompendium.util.rootModule
import org.leafygreens.kompendium.util.statusPageModule
import org.leafygreens.kompendium.util.statusPageMultiExceptions
import org.leafygreens.kompendium.util.trailingSlash
import org.leafygreens.kompendium.util.withDefaultParameter
import org.leafygreens.kompendium.util.withExamples
internal class KompendiumTest { internal class KompendiumTest {
@ -448,305 +436,6 @@ internal class KompendiumTest {
} }
} }
private companion object {
val testGetResponse = ResponseInfo<TestResponse>(KompendiumHttpCodes.OK, "A Successful Endeavor")
val testGetListResponse = ResponseInfo<List<TestResponse>>(KompendiumHttpCodes.OK, "A Successful List-y Endeavor")
val testPostResponse = ResponseInfo<TestCreatedResponse>(KompendiumHttpCodes.CREATED, "A Successful Endeavor")
val testPostResponseAgain = ResponseInfo<Boolean>(KompendiumHttpCodes.CREATED, "A Successful Endeavor")
val testDeleteResponse =
ResponseInfo<Unit>(KompendiumHttpCodes.NO_CONTENT, "A Successful Endeavor", mediaTypes = emptyList())
val testRequest = RequestInfo<TestRequest>("A Test request")
val testRequestAgain = RequestInfo<Int>("A Test request")
val complexRequest = RequestInfo<ComplexRequest>("A Complex request")
val testGetInfo = GetInfo<TestParams, TestResponse>(
summary = "Another get test",
description = "testing more",
responseInfo = testGetResponse
)
val testGetInfoAgain = GetInfo<TestParams, List<TestResponse>>(
summary = "Another get test",
description = "testing more",
responseInfo = testGetListResponse
)
val testGetWithException = testGetInfo.copy(
canThrow = setOf(Exception::class)
)
val testGetWithMultipleExceptions = testGetInfo.copy(
canThrow = setOf(AccessDeniedException::class, Exception::class)
)
val testPostInfo = PostInfo<TestParams, TestRequest, TestCreatedResponse>(
summary = "Test post endpoint",
description = "Post your tests here!",
responseInfo = testPostResponse,
requestInfo = testRequest
)
val testPutInfo = PutInfo<Unit, ComplexRequest, TestCreatedResponse>(
summary = "Test put endpoint",
description = "Put your tests here!",
responseInfo = testPostResponse,
requestInfo = complexRequest
)
val testPutInfoAlso = PutInfo<TestParams, TestRequest, TestCreatedResponse>(
summary = "Test put endpoint",
description = "Put your tests here!",
responseInfo = testPostResponse,
requestInfo = testRequest
)
val testPutInfoAgain = PutInfo<Unit, Int, Boolean>(
summary = "Test put endpoint",
description = "Put your tests here!",
responseInfo = testPostResponseAgain,
requestInfo = testRequestAgain
)
val testDeleteInfo = DeleteInfo<TestParams, Unit>(
summary = "Test delete endpoint",
description = "testing my deletes",
responseInfo = testDeleteResponse
)
val emptyTestGetInfo =
GetInfo<OptionalParams, Unit>(summary = "No request params and response body", description = "testing more")
val trulyEmptyTestGetInfo =
GetInfo<Unit, Unit>(summary = "No request params and response body", description = "testing more")
}
private fun Application.configModule() {
install(ContentNegotiation) {
jackson {
enable(SerializationFeature.INDENT_OUTPUT)
setSerializationInclusion(JsonInclude.Include.NON_NULL)
}
}
}
private fun Application.statusPageModule() {
install(StatusPages) {
notarizedException<Exception, ExceptionResponse>(info = ResponseInfo(400, "Bad Things Happened")) {
call.respond(HttpStatusCode.BadRequest, ExceptionResponse("Why you do dis?"))
}
}
}
private fun Application.statusPageMultiExceptions() {
install(StatusPages) {
notarizedException<AccessDeniedException, Unit>(info = ResponseInfo(403, "New API who dis?")) {
call.respond(HttpStatusCode.Forbidden)
}
notarizedException<Exception, ExceptionResponse>(info = ResponseInfo(400, "Bad Things Happened")) {
call.respond(HttpStatusCode.BadRequest, ExceptionResponse("Why you do dis?"))
}
}
}
private fun Application.notarizedGetWithNotarizedException() {
routing {
route("/test") {
notarizedGet(testGetWithException) {
error("something terrible has happened!")
}
}
}
}
private fun Application.notarizedGetWithMultipleThrowables() {
routing {
route("/test") {
notarizedGet(testGetWithMultipleExceptions) {
error("something terrible has happened!")
}
}
}
}
private fun Application.notarizedGetModule() {
routing {
route("/test") {
notarizedGet(testGetInfo) {
call.respondText { "hey dude ‼️ congratz on the get request" }
}
}
}
}
private fun Application.notarizedPostModule() {
routing {
route("/test") {
notarizedPost(testPostInfo) {
call.respondText { "hey dude ✌️ congratz on the post request" }
}
}
}
}
private fun Application.notarizedDeleteModule() {
routing {
route("/test") {
notarizedDelete(testDeleteInfo) {
call.respond(HttpStatusCode.NoContent)
}
}
}
}
private fun Application.notarizedPutModule() {
routing {
route("/test") {
notarizedPut(testPutInfoAlso) {
call.respondText { "hey pal 🌝 whatcha doin' here?" }
}
}
}
}
private fun Application.pathParsingTestModule() {
routing {
route("/this") {
route("/is") {
route("/a") {
route("/complex") {
route("path") {
route("with/an/{id}") {
notarizedGet(testGetInfo) {
call.respondText { "Aww you followed this whole route 🥺" }
}
}
}
}
}
}
}
}
}
private fun Application.rootModule() {
routing {
route("/") {
notarizedGet(testGetInfo) {
call.respondText { "☎️🏠🌲" }
}
}
}
}
private fun Application.nestedUnderRootModule() {
routing {
route("/") {
route("/testerino") {
notarizedGet(testGetInfo) {
call.respondText { "🤔🔥" }
}
}
}
}
}
private fun Application.trailingSlash() {
routing {
route("/test") {
route("/") {
notarizedGet(testGetInfo) {
call.respondText { "🙀👾" }
}
}
}
}
}
private fun Application.returnsList() {
routing {
route("/test") {
notarizedGet(testGetInfoAgain) {
call.respondText { "hey dude ur doing amazing work!" }
}
}
}
}
private fun Application.complexType() {
routing {
route("/test") {
notarizedPut(testPutInfo) {
call.respondText { "heya" }
}
}
}
}
private fun Application.primitives() {
routing {
route("/test") {
notarizedPut(testPutInfoAgain) {
call.respondText { "heya" }
}
}
}
}
private fun Application.emptyGet() {
routing {
route("/test/empty") {
notarizedGet(trulyEmptyTestGetInfo) {
call.respond(HttpStatusCode.OK)
}
}
}
}
private fun Application.withExamples() {
routing {
route("/test/examples") {
notarizedPost(
info = PostInfo<Unit, TestRequest, TestResponse>(
summary = "Example Parameters",
description = "A test for setting parameter examples",
requestInfo = RequestInfo(
description = "Test",
examples = mapOf(
"one" to TestRequest(fieldName = TestNested(nesty = "hey"), b = 4.0, aaa = emptyList()),
"two" to TestRequest(fieldName = TestNested(nesty = "hello"), b = 3.8, aaa = listOf(31324234))
)
),
responseInfo = ResponseInfo(
status = 201,
description = "nice",
examples = mapOf("test" to TestResponse(c = "spud"))
),
)
) {
call.respond(HttpStatusCode.OK)
}
}
}
}
private fun Application.withDefaultParameter() {
routing {
route("/test") {
notarizedGet(
info = GetInfo<DefaultParameter, TestResponse>(
summary = "Testing Default Params",
description = "Should have a default parameter value"
)
) {
call.respond(TestResponse("hey"))
}
}
}
}
data class OptionalParams(
@KompendiumParam(ParamType.QUERY) val required: String,
@KompendiumParam(ParamType.QUERY) val notRequired: String?
)
private fun Application.nonRequiredParamsGet() {
routing {
route("/test/optional") {
notarizedGet(emptyTestGetInfo) {
call.respond(HttpStatusCode.OK)
}
}
}
}
private val oas = Kompendium.openApiSpec.copy( private val oas = Kompendium.openApiSpec.copy(
info = OpenApiSpecInfo( info = OpenApiSpecInfo(

View File

@ -42,18 +42,12 @@ data class TestResponse(val c: String)
data class TestCreatedResponse(val id: Int, val c: String) data class TestCreatedResponse(val id: Int, val c: String)
object TestDeleteResponse
data class ComplexRequest( data class ComplexRequest(
val org: String, val org: String,
@KompendiumField("amazing_field") @KompendiumField("amazing_field")
val amazingField: String, val amazingField: String,
val tables: List<NestedComplexItem> val tables: List<NestedComplexItem>
) { )
fun testThing() {
println("hey mom 👋")
}
}
data class NestedComplexItem( data class NestedComplexItem(
val name: String, val name: String,
@ -75,10 +69,9 @@ data class DefaultParameter(
@KompendiumParam(ParamType.PATH) val c: Boolean @KompendiumParam(ParamType.PATH) val c: Boolean
) )
sealed class TestSealedClass(open val a: String)
data class SimpleTSC(val b: Int) : TestSealedClass("hey")
open class MediumTSC(override val a: String, val b: Int) : TestSealedClass(a)
data class WildTSC(val c: Boolean, val d: String, val e: Int) : MediumTSC(d, e)
data class ExceptionResponse(val message: String) data class ExceptionResponse(val message: String)
data class OptionalParams(
@KompendiumParam(ParamType.QUERY) val required: String,
@KompendiumParam(ParamType.QUERY) val notRequired: String?
)

View File

@ -0,0 +1,257 @@
package org.leafygreens.kompendium.util
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.features.ContentNegotiation
import io.ktor.features.StatusPages
import io.ktor.http.HttpStatusCode
import io.ktor.jackson.jackson
import io.ktor.response.respond
import io.ktor.response.respondText
import io.ktor.routing.route
import io.ktor.routing.routing
import org.leafygreens.kompendium.Notarized.notarizedDelete
import org.leafygreens.kompendium.Notarized.notarizedException
import org.leafygreens.kompendium.Notarized.notarizedGet
import org.leafygreens.kompendium.Notarized.notarizedPost
import org.leafygreens.kompendium.Notarized.notarizedPut
import org.leafygreens.kompendium.models.meta.MethodInfo
import org.leafygreens.kompendium.models.meta.RequestInfo
import org.leafygreens.kompendium.models.meta.ResponseInfo
fun Application.configModule() {
install(ContentNegotiation) {
jackson {
enable(SerializationFeature.INDENT_OUTPUT)
setSerializationInclusion(JsonInclude.Include.NON_NULL)
}
}
}
fun Application.statusPageModule() {
install(StatusPages) {
notarizedException<Exception, ExceptionResponse>(info = ResponseInfo(HttpStatusCode.BadRequest, "Bad Things Happened")) {
call.respond(HttpStatusCode.BadRequest, ExceptionResponse("Why you do dis?"))
}
}
}
fun Application.statusPageMultiExceptions() {
install(StatusPages) {
notarizedException<AccessDeniedException, Unit>(info = ResponseInfo(HttpStatusCode.Forbidden, "New API who dis?")) {
call.respond(HttpStatusCode.Forbidden)
}
notarizedException<Exception, ExceptionResponse>(info = ResponseInfo(HttpStatusCode.BadRequest, "Bad Things Happened")) {
call.respond(HttpStatusCode.BadRequest, ExceptionResponse("Why you do dis?"))
}
}
}
fun Application.notarizedGetWithNotarizedException() {
routing {
route("/test") {
notarizedGet(TestResponseInfo.testGetWithException) {
error("something terrible has happened!")
}
}
}
}
fun Application.notarizedGetWithMultipleThrowables() {
routing {
route("/test") {
notarizedGet(TestResponseInfo.testGetWithMultipleExceptions) {
error("something terrible has happened!")
}
}
}
}
fun Application.notarizedGetModule() {
routing {
route("/test") {
notarizedGet(TestResponseInfo.testGetInfo) {
call.respondText { "hey dude ‼️ congratz on the get request" }
}
}
}
}
fun Application.notarizedPostModule() {
routing {
route("/test") {
notarizedPost(TestResponseInfo.testPostInfo) {
call.respondText { "hey dude ✌️ congratz on the post request" }
}
}
}
}
fun Application.notarizedDeleteModule() {
routing {
route("/test") {
notarizedDelete(TestResponseInfo.testDeleteInfo) {
call.respond(HttpStatusCode.NoContent)
}
}
}
}
fun Application.notarizedPutModule() {
routing {
route("/test") {
notarizedPut(TestResponseInfo.testPutInfoAlso) {
call.respondText { "hey pal 🌝 whatcha doin' here?" }
}
}
}
}
fun Application.pathParsingTestModule() {
routing {
route("/this") {
route("/is") {
route("/a") {
route("/complex") {
route("path") {
route("with/an/{id}") {
notarizedGet(TestResponseInfo.testGetInfo) {
call.respondText { "Aww you followed this whole route 🥺" }
}
}
}
}
}
}
}
}
}
fun Application.rootModule() {
routing {
route("/") {
notarizedGet(TestResponseInfo.testGetInfo) {
call.respondText { "☎️🏠🌲" }
}
}
}
}
fun Application.nestedUnderRootModule() {
routing {
route("/") {
route("/testerino") {
notarizedGet(TestResponseInfo.testGetInfo) {
call.respondText { "🤔🔥" }
}
}
}
}
}
fun Application.trailingSlash() {
routing {
route("/test") {
route("/") {
notarizedGet(TestResponseInfo.testGetInfo) {
call.respondText { "🙀👾" }
}
}
}
}
}
fun Application.returnsList() {
routing {
route("/test") {
notarizedGet(TestResponseInfo.testGetInfoAgain) {
call.respondText { "hey dude ur doing amazing work!" }
}
}
}
}
fun Application.complexType() {
routing {
route("/test") {
notarizedPut(TestResponseInfo.testPutInfo) {
call.respondText { "heya" }
}
}
}
}
fun Application.primitives() {
routing {
route("/test") {
notarizedPut(TestResponseInfo.testPutInfoAgain) {
call.respondText { "heya" }
}
}
}
}
fun Application.emptyGet() {
routing {
route("/test/empty") {
notarizedGet(TestResponseInfo.trulyEmptyTestGetInfo) {
call.respond(HttpStatusCode.OK)
}
}
}
}
fun Application.withExamples() {
routing {
route("/test/examples") {
notarizedPost(
info = MethodInfo.PostInfo<Unit, TestRequest, TestResponse>(
summary = "Example Parameters",
description = "A test for setting parameter examples",
requestInfo = RequestInfo(
description = "Test",
examples = mapOf(
"one" to TestRequest(fieldName = TestNested(nesty = "hey"), b = 4.0, aaa = emptyList()),
"two" to TestRequest(fieldName = TestNested(nesty = "hello"), b = 3.8, aaa = listOf(31324234))
)
),
responseInfo = ResponseInfo(
status = HttpStatusCode.Created,
description = "nice",
examples = mapOf("test" to TestResponse(c = "spud"))
),
)
) {
call.respond(HttpStatusCode.OK)
}
}
}
}
fun Application.withDefaultParameter() {
routing {
route("/test") {
notarizedGet(
info = MethodInfo.GetInfo<DefaultParameter, TestResponse>(
summary = "Testing Default Params",
description = "Should have a default parameter value"
)
) {
call.respond(TestResponse("hey"))
}
}
}
}
fun Application.nonRequiredParamsGet() {
routing {
route("/test/optional") {
notarizedGet(TestResponseInfo.emptyTestGetInfo) {
call.respond(HttpStatusCode.OK)
}
}
}
}

View File

@ -0,0 +1,71 @@
package org.leafygreens.kompendium.util
import io.ktor.http.HttpStatusCode
import org.leafygreens.kompendium.models.meta.MethodInfo
import org.leafygreens.kompendium.models.meta.RequestInfo
import org.leafygreens.kompendium.models.meta.ResponseInfo
object TestResponseInfo {
private val testGetResponse = ResponseInfo<TestResponse>(HttpStatusCode.OK, "A Successful Endeavor")
private val testGetListResponse =
ResponseInfo<List<TestResponse>>(HttpStatusCode.OK, "A Successful List-y Endeavor")
private val testPostResponse = ResponseInfo<TestCreatedResponse>(HttpStatusCode.Created, "A Successful Endeavor")
private val testPostResponseAgain = ResponseInfo<Boolean>(HttpStatusCode.Created, "A Successful Endeavor")
private val testDeleteResponse =
ResponseInfo<Unit>(HttpStatusCode.NoContent, "A Successful Endeavor", mediaTypes = emptyList())
private val testRequest = RequestInfo<TestRequest>("A Test request")
private val testRequestAgain = RequestInfo<Int>("A Test request")
private val complexRequest = RequestInfo<ComplexRequest>("A Complex request")
val testGetInfo = MethodInfo.GetInfo<TestParams, TestResponse>(
summary = "Another get test",
description = "testing more",
responseInfo = testGetResponse
)
val testGetInfoAgain = MethodInfo.GetInfo<TestParams, List<TestResponse>>(
summary = "Another get test",
description = "testing more",
responseInfo = testGetListResponse
)
val testGetWithException = testGetInfo.copy(
canThrow = setOf(Exception::class)
)
val testGetWithMultipleExceptions = testGetInfo.copy(
canThrow = setOf(AccessDeniedException::class, Exception::class)
)
val testPostInfo = MethodInfo.PostInfo<TestParams, TestRequest, TestCreatedResponse>(
summary = "Test post endpoint",
description = "Post your tests here!",
responseInfo = testPostResponse,
requestInfo = testRequest
)
val testPutInfo = MethodInfo.PutInfo<Unit, ComplexRequest, TestCreatedResponse>(
summary = "Test put endpoint",
description = "Put your tests here!",
responseInfo = testPostResponse,
requestInfo = complexRequest
)
val testPutInfoAlso = MethodInfo.PutInfo<TestParams, TestRequest, TestCreatedResponse>(
summary = "Test put endpoint",
description = "Put your tests here!",
responseInfo = testPostResponse,
requestInfo = testRequest
)
val testPutInfoAgain = MethodInfo.PutInfo<Unit, Int, Boolean>(
summary = "Test put endpoint",
description = "Put your tests here!",
responseInfo = testPostResponseAgain,
requestInfo = testRequestAgain
)
val testDeleteInfo = MethodInfo.DeleteInfo<TestParams, Unit>(
summary = "Test delete endpoint",
description = "testing my deletes",
responseInfo = testDeleteResponse
)
val emptyTestGetInfo =
MethodInfo.GetInfo<OptionalParams, Unit>(
summary = "No request params and response body",
description = "testing more"
)
val trulyEmptyTestGetInfo =
MethodInfo.GetInfo<Unit, Unit>(summary = "No request params and response body", description = "testing more")
}

View File

@ -38,7 +38,6 @@ import org.leafygreens.kompendium.playground.PlaygroundToC.testSinglePutInfo
import org.leafygreens.kompendium.routes.openApi import org.leafygreens.kompendium.routes.openApi
import org.leafygreens.kompendium.routes.redoc import org.leafygreens.kompendium.routes.redoc
import org.leafygreens.kompendium.swagger.swaggerUI import org.leafygreens.kompendium.swagger.swaggerUI
import org.leafygreens.kompendium.util.KompendiumHttpCodes
fun main() { fun main() {
embeddedServer( embeddedServer(
@ -74,7 +73,7 @@ fun Application.configModule() {
install(StatusPages) { install(StatusPages) {
notarizedException<Exception, ExceptionResponse>( notarizedException<Exception, ExceptionResponse>(
info = ResponseInfo( info = ResponseInfo(
KompendiumHttpCodes.BAD_REQUEST, HttpStatusCode.BadRequest,
"Bad Things Happened", "Bad Things Happened",
examples = mapOf("example" to ExceptionResponse("hey bad things happened sorry")) examples = mapOf("example" to ExceptionResponse("hey bad things happened sorry"))
) )

View File

@ -1,16 +1,16 @@
package org.leafygreens.kompendium.playground package org.leafygreens.kompendium.playground
import io.ktor.http.HttpStatusCode
import org.leafygreens.kompendium.models.meta.MethodInfo import org.leafygreens.kompendium.models.meta.MethodInfo
import org.leafygreens.kompendium.models.meta.RequestInfo import org.leafygreens.kompendium.models.meta.RequestInfo
import org.leafygreens.kompendium.models.meta.ResponseInfo import org.leafygreens.kompendium.models.meta.ResponseInfo
import org.leafygreens.kompendium.util.KompendiumHttpCodes
object PlaygroundToC { object PlaygroundToC {
val testGetWithExamples = MethodInfo.GetInfo<Unit, ExampleResponse>( val testGetWithExamples = MethodInfo.GetInfo<Unit, ExampleResponse>(
summary = "Example Parameters", summary = "Example Parameters",
description = "A test for setting parameter examples", description = "A test for setting parameter examples",
responseInfo = ResponseInfo( responseInfo = ResponseInfo(
status = 200, status = HttpStatusCode.OK,
description = "nice", description = "nice",
examples = mapOf("test" to ExampleResponse(c = "spud")) examples = mapOf("test" to ExampleResponse(c = "spud"))
), ),
@ -27,7 +27,7 @@ object PlaygroundToC {
) )
), ),
responseInfo = ResponseInfo( responseInfo = ResponseInfo(
status = KompendiumHttpCodes.CREATED, status = HttpStatusCode.Created,
description = "Congratz you hit da endpoint", description = "Congratz you hit da endpoint",
examples = mapOf( examples = mapOf(
"Expect This" to ExampleResponse(c = "Hi"), "Expect This" to ExampleResponse(c = "Hi"),
@ -42,7 +42,7 @@ object PlaygroundToC {
description = "Test for the getting", description = "Test for the getting",
tags = setOf("test", "sample", "get"), tags = setOf("test", "sample", "get"),
responseInfo = ResponseInfo( responseInfo = ResponseInfo(
status = KompendiumHttpCodes.OK, status = HttpStatusCode.OK,
description = "Returns sample info" description = "Returns sample info"
) )
) )
@ -51,7 +51,7 @@ object PlaygroundToC {
description = "testing more", description = "testing more",
tags = setOf("anotherTest", "sample"), tags = setOf("anotherTest", "sample"),
responseInfo = ResponseInfo( responseInfo = ResponseInfo(
status = KompendiumHttpCodes.OK, status = HttpStatusCode.OK,
description = "Returns a different sample" description = "Returns a different sample"
) )
) )
@ -66,7 +66,7 @@ object PlaygroundToC {
description = "Simple request body" description = "Simple request body"
), ),
responseInfo = ResponseInfo( responseInfo = ResponseInfo(
status = KompendiumHttpCodes.CREATED, status = HttpStatusCode.Created,
description = "Worlds most complex response" description = "Worlds most complex response"
) )
) )
@ -77,7 +77,7 @@ object PlaygroundToC {
description = "Info needed to perform this put request" description = "Info needed to perform this put request"
), ),
responseInfo = ResponseInfo( responseInfo = ResponseInfo(
status = KompendiumHttpCodes.CREATED, status = HttpStatusCode.Created,
description = "What we give you when u do the puts" description = "What we give you when u do the puts"
) )
) )
@ -85,7 +85,7 @@ object PlaygroundToC {
summary = "Test delete endpoint", summary = "Test delete endpoint",
description = "testing my deletes", description = "testing my deletes",
responseInfo = ResponseInfo( responseInfo = ResponseInfo(
status = KompendiumHttpCodes.NO_CONTENT, status = HttpStatusCode.NoContent,
description = "Signifies that your item was deleted successfully", description = "Signifies that your item was deleted successfully",
mediaTypes = emptyList() mediaTypes = emptyList()
) )
@ -95,7 +95,7 @@ object PlaygroundToC {
description = "testing more", description = "testing more",
tags = setOf("anotherTest", "sample"), tags = setOf("anotherTest", "sample"),
responseInfo = ResponseInfo( responseInfo = ResponseInfo(
status = KompendiumHttpCodes.OK, status = HttpStatusCode.OK,
description = "Returns a different sample" description = "Returns a different sample"
), ),
securitySchemes = setOf("basic") securitySchemes = setOf("basic")