V1 beta release time 🥳 (#48)
This commit is contained in:
25
.github/workflows/release.yml
vendored
Normal file
25
.github/workflows/release.yml
vendored
Normal 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 }}
|
12
CHANGELOG.md
12
CHANGELOG.md
@ -1,5 +1,17 @@
|
||||
# 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
|
||||
|
||||
### Added
|
||||
|
24
README.md
24
README.md
@ -37,16 +37,6 @@ dependencies {
|
||||
|
||||
## 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
|
||||
|
||||
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.
|
||||
|
||||
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)
|
||||
|
@ -1,5 +1,5 @@
|
||||
# Kompendium
|
||||
project.version=0.9.0
|
||||
project.version=1.0.0-beta
|
||||
# Kotlin
|
||||
kotlin.code.style=official
|
||||
# Gradle
|
||||
|
@ -11,6 +11,7 @@ import io.ktor.auth.authenticate
|
||||
import io.ktor.features.ContentNegotiation
|
||||
import io.ktor.http.ContentType
|
||||
import io.ktor.http.HttpMethod
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.jackson.jackson
|
||||
import io.ktor.response.respondText
|
||||
import io.ktor.routing.route
|
||||
@ -31,7 +32,6 @@ import org.leafygreens.kompendium.models.meta.MethodInfo
|
||||
import org.leafygreens.kompendium.models.meta.ResponseInfo
|
||||
import org.leafygreens.kompendium.routes.openApi
|
||||
import org.leafygreens.kompendium.routes.redoc
|
||||
import org.leafygreens.kompendium.util.KompendiumHttpCodes
|
||||
|
||||
internal class KompendiumAuthTest {
|
||||
|
||||
@ -189,7 +189,7 @@ internal class KompendiumAuthTest {
|
||||
}
|
||||
|
||||
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) =
|
||||
MethodInfo.GetInfo<TestParams, TestResponse>(
|
||||
summary = "Another get test",
|
||||
|
@ -7,6 +7,9 @@ import org.leafygreens.kompendium.models.oas.OpenApiSpecInfo
|
||||
import org.leafygreens.kompendium.path.CorePathCalculator
|
||||
import org.leafygreens.kompendium.path.PathCalculator
|
||||
|
||||
/**
|
||||
* Maintains all state for the Kompendium library
|
||||
*/
|
||||
object Kompendium {
|
||||
|
||||
var errorMap: ErrorMap = emptyMap()
|
||||
|
@ -4,8 +4,19 @@ import io.ktor.routing.Route
|
||||
import kotlin.reflect.KType
|
||||
import kotlin.reflect.typeOf
|
||||
|
||||
/**
|
||||
* Functions are considered preflight when they are used to intercept a method ahead of running.
|
||||
*/
|
||||
object KompendiumPreFlight {
|
||||
|
||||
/**
|
||||
* Performs all content analysis on the types provided to a notarized route and adds it to the top level spec
|
||||
* @param TParam
|
||||
* @param TReq
|
||||
* @param TResp
|
||||
* @param block The function to execute, provided type information of the parameters above
|
||||
* @return [Route]
|
||||
*/
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
inline fun <reified TParam : Any, reified TReq : Any, reified TResp : Any> methodNotarizationPreFlight(
|
||||
block: (KType, KType, KType) -> Route
|
||||
@ -20,6 +31,12 @@ object KompendiumPreFlight {
|
||||
return block.invoke(paramType, requestType, responseType)
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs all content analysis on the types provided to a notarized error and adds them to the top level spec.
|
||||
* @param TErr
|
||||
* @param TResp
|
||||
* @param block The function to execute, provided type information of the parameters above
|
||||
*/
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
inline fun <reified TErr: Throwable, reified TResp : Any> errorNotarizationPreFlight(
|
||||
block: (KType, KType) -> Unit
|
||||
|
@ -21,10 +21,19 @@ import org.leafygreens.kompendium.util.Helpers.getReferenceSlug
|
||||
import org.leafygreens.kompendium.util.Helpers.logged
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
/**
|
||||
* Responsible for generating the schema map that is used to power all object references across the API Spec.
|
||||
*/
|
||||
object Kontent {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(javaClass)
|
||||
|
||||
/**
|
||||
* Analyzes a type [T] for its top-level and any nested schemas, and adds them to a [SchemaMap], if provided
|
||||
* @param T type to analyze
|
||||
* @param cache Existing schema map to append to
|
||||
* @return an updated schema map containing all type information for [T]
|
||||
*/
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
inline fun <reified T> generateKontent(
|
||||
cache: SchemaMap = emptyMap()
|
||||
@ -33,6 +42,12 @@ object Kontent {
|
||||
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)
|
||||
inline fun <reified T> generateParameterKontent(
|
||||
cache: SchemaMap = emptyMap()
|
||||
@ -42,6 +57,11 @@ object Kontent {
|
||||
.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(
|
||||
type: KType,
|
||||
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 =
|
||||
when (cache.containsKey(clazz.simpleName)) {
|
||||
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 {
|
||||
val options = clazz.java.enumConstants.map { it.toString() }.toSet()
|
||||
return cache.plus(clazz.simpleName!! to EnumSchema(options))
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for when a [Map] is encountered
|
||||
* @param type Map type information
|
||||
* @param clazz Map class information
|
||||
* @param cache Existing schema map to append to
|
||||
*/
|
||||
private fun handleMapType(type: KType, clazz: KClass<*>, cache: SchemaMap): SchemaMap {
|
||||
logger.debug("Map detected for $type, generating schema and appending to cache")
|
||||
val (keyType, valType) = type.arguments.map { it.type }
|
||||
@ -112,6 +148,12 @@ object Kontent {
|
||||
return updatedCache.plus(referenceName to schema)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for when a [Collection] is encountered
|
||||
* @param type Collection type information
|
||||
* @param clazz Collection class information
|
||||
* @param cache Existing schema map to append to
|
||||
*/
|
||||
private fun handleCollectionType(type: KType, clazz: KClass<*>, cache: SchemaMap): SchemaMap {
|
||||
logger.debug("Collection detected for $type, generating schema and appending to cache")
|
||||
val collectionType = type.arguments.first().type!!
|
||||
|
@ -26,7 +26,19 @@ import org.leafygreens.kompendium.util.Helpers
|
||||
import org.leafygreens.kompendium.util.Helpers.getReferenceSlug
|
||||
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 {
|
||||
|
||||
/**
|
||||
* Generates the OpenAPI Path spec from provided metadata
|
||||
* @param info implementation of the [MethodInfo] sealed class
|
||||
* @param paramType Type of `TParam`
|
||||
* @param requestType Type of `TReq` if required
|
||||
* @param responseType Type of `TResp`
|
||||
* @return object representing the OpenAPI Path spec.
|
||||
*/
|
||||
fun parseMethodInfo(
|
||||
info: MethodInfo<*, *>,
|
||||
paramType: KType,
|
||||
@ -61,19 +73,34 @@ object MethodParser {
|
||||
) else null
|
||||
)
|
||||
|
||||
private fun parseThrowables(throwables: Set<KClass<*>>): Map<Int, OpenApiSpecReferencable> = throwables.mapNotNull {
|
||||
Kompendium.errorMap[it.createType()]
|
||||
}.toMap()
|
||||
|
||||
fun <TResp> ResponseInfo<TResp>.parseErrorInfo(
|
||||
/**
|
||||
* Adds the error to the [Kompendium.errorMap] for reference in notarized routes.
|
||||
* @param errorType [KType] of the throwable being handled
|
||||
* @param responseType [KType] the type of the response sent in event of error
|
||||
*/
|
||||
fun ResponseInfo<*>.parseErrorInfo(
|
||||
errorType: KType,
|
||||
responseType: KType
|
||||
) {
|
||||
Kompendium.errorMap = Kompendium.errorMap.plus(errorType to responseType.toResponseSpec(this))
|
||||
}
|
||||
|
||||
// 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) {
|
||||
null -> null
|
||||
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) {
|
||||
null -> null // TODO again probably revisit this
|
||||
null -> null
|
||||
else -> {
|
||||
val specResponse = OpenApiSpecResponse(
|
||||
description = responseInfo.description,
|
||||
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(
|
||||
mediaTypes: List<String>,
|
||||
examples: Map<String, F>
|
||||
@ -111,6 +151,13 @@ object MethodParser {
|
||||
} 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> {
|
||||
val clazz = classifier as KClass<*>
|
||||
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? {
|
||||
val constructor = clazz.primaryConstructor
|
||||
val parameterInQuestion = constructor
|
||||
@ -152,6 +205,12 @@ object MethodParser {
|
||||
return getterFunction.invoke(instance)
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows the reflection invoker to populate a parameter map with values in order to sus out any default parameters.
|
||||
* @param param Parameter to provide value for
|
||||
* @return value of the proper type to match param
|
||||
* @throws [IllegalStateException] if parameter type is not one of the basic types supported below.
|
||||
*/
|
||||
private fun defaultValueInjector(param: KParameter): Any = when (param.type.classifier) {
|
||||
String::class -> "test"
|
||||
Boolean::class -> false
|
||||
@ -162,5 +221,4 @@ object MethodParser {
|
||||
UUID::class -> UUID.randomUUID()
|
||||
else -> error("Unsupported Type")
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -17,9 +17,20 @@ import org.leafygreens.kompendium.models.meta.MethodInfo.DeleteInfo
|
||||
import org.leafygreens.kompendium.models.meta.ResponseInfo
|
||||
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 {
|
||||
|
||||
@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(
|
||||
info: GetInfo<TParam, TResp>,
|
||||
noinline body: PipelineInterceptor<Unit, ApplicationCall>
|
||||
@ -31,6 +42,14 @@ object Notarized {
|
||||
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(
|
||||
info: PostInfo<TParam, TReq, TResp>,
|
||||
noinline body: PipelineInterceptor<Unit, ApplicationCall>
|
||||
@ -42,6 +61,14 @@ object Notarized {
|
||||
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(
|
||||
info: PutInfo<TParam, TReq, TResp>,
|
||||
noinline body: PipelineInterceptor<Unit, ApplicationCall>,
|
||||
@ -54,6 +81,13 @@ object Notarized {
|
||||
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(
|
||||
info: DeleteInfo<TParam, TResp>,
|
||||
noinline body: PipelineInterceptor<Unit, ApplicationCall>
|
||||
@ -65,6 +99,12 @@ object Notarized {
|
||||
return method(HttpMethod.Delete) { handle(body) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Notarization for a handled exception response
|
||||
* @param TErr The [Throwable] that is being handled
|
||||
* @param TResp Class detailing the expected API response when handled
|
||||
* @param info Response metadata
|
||||
*/
|
||||
inline fun <reified TErr : Throwable, reified TResp : Any> StatusPages.Configuration.notarizedException(
|
||||
info: ResponseInfo<TResp>,
|
||||
noinline handler: suspend PipelineContext<Unit, ApplicationCall>.(TErr) -> Unit
|
||||
|
@ -1,7 +1,9 @@
|
||||
package org.leafygreens.kompendium.models.meta
|
||||
|
||||
import io.ktor.http.HttpStatusCode
|
||||
|
||||
data class ResponseInfo<TResp>(
|
||||
val status: Int,
|
||||
val status: HttpStatusCode,
|
||||
val description: String,
|
||||
val mediaTypes: List<String> = listOf("application/json"),
|
||||
val examples: Map<String, TResp> = emptyMap()
|
||||
|
@ -7,6 +7,9 @@ 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)
|
||||
|
@ -2,10 +2,19 @@ package org.leafygreens.kompendium.path
|
||||
|
||||
import io.ktor.routing.Route
|
||||
|
||||
/**
|
||||
* Extensible interface for calculating Ktor paths
|
||||
*/
|
||||
interface PathCalculator {
|
||||
|
||||
/**
|
||||
* Core route calculation method
|
||||
*/
|
||||
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
|
||||
|
||||
}
|
||||
|
@ -7,6 +7,10 @@ import io.ktor.routing.get
|
||||
import io.ktor.routing.route
|
||||
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) {
|
||||
route("/openapi.json") {
|
||||
get {
|
||||
|
@ -15,7 +15,12 @@ import kotlinx.html.title
|
||||
import kotlinx.html.unsafe
|
||||
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") {
|
||||
get {
|
||||
call.respondHtml {
|
||||
@ -41,8 +46,7 @@ fun Routing.redoc(oas: OpenApiSpec) {
|
||||
}
|
||||
}
|
||||
body {
|
||||
// TODO needs to mirror openApi route
|
||||
unsafe { +"<redoc spec-url='/openapi.json'></redoc>" }
|
||||
unsafe { +"<redoc spec-url='${specUrl}'></redoc>" }
|
||||
script {
|
||||
src = "https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js"
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
}
|
@ -1,18 +1,8 @@
|
||||
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.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.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.server.testing.handleRequest
|
||||
import io.ktor.server.testing.withTestApplication
|
||||
@ -20,35 +10,33 @@ import java.net.URI
|
||||
import kotlin.test.AfterTest
|
||||
import kotlin.test.Test
|
||||
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.OpenApiSpecInfoContact
|
||||
import org.leafygreens.kompendium.models.oas.OpenApiSpecInfoLicense
|
||||
import org.leafygreens.kompendium.models.oas.OpenApiSpecServer
|
||||
import org.leafygreens.kompendium.routes.openApi
|
||||
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.TestNested
|
||||
import org.leafygreens.kompendium.util.TestParams
|
||||
import org.leafygreens.kompendium.util.TestRequest
|
||||
import org.leafygreens.kompendium.util.TestResponse
|
||||
import org.leafygreens.kompendium.util.complexType
|
||||
import org.leafygreens.kompendium.util.configModule
|
||||
import org.leafygreens.kompendium.util.emptyGet
|
||||
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 {
|
||||
|
||||
@ -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(
|
||||
info = OpenApiSpecInfo(
|
||||
|
@ -42,18 +42,12 @@ data class TestResponse(val c: String)
|
||||
|
||||
data class TestCreatedResponse(val id: Int, val c: String)
|
||||
|
||||
object TestDeleteResponse
|
||||
|
||||
data class ComplexRequest(
|
||||
val org: String,
|
||||
@KompendiumField("amazing_field")
|
||||
val amazingField: String,
|
||||
val tables: List<NestedComplexItem>
|
||||
) {
|
||||
fun testThing() {
|
||||
println("hey mom 👋")
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
data class NestedComplexItem(
|
||||
val name: String,
|
||||
@ -75,10 +69,9 @@ data class DefaultParameter(
|
||||
@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 OptionalParams(
|
||||
@KompendiumParam(ParamType.QUERY) val required: String,
|
||||
@KompendiumParam(ParamType.QUERY) val notRequired: String?
|
||||
)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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")
|
||||
}
|
@ -38,7 +38,6 @@ import org.leafygreens.kompendium.playground.PlaygroundToC.testSinglePutInfo
|
||||
import org.leafygreens.kompendium.routes.openApi
|
||||
import org.leafygreens.kompendium.routes.redoc
|
||||
import org.leafygreens.kompendium.swagger.swaggerUI
|
||||
import org.leafygreens.kompendium.util.KompendiumHttpCodes
|
||||
|
||||
fun main() {
|
||||
embeddedServer(
|
||||
@ -74,7 +73,7 @@ fun Application.configModule() {
|
||||
install(StatusPages) {
|
||||
notarizedException<Exception, ExceptionResponse>(
|
||||
info = ResponseInfo(
|
||||
KompendiumHttpCodes.BAD_REQUEST,
|
||||
HttpStatusCode.BadRequest,
|
||||
"Bad Things Happened",
|
||||
examples = mapOf("example" to ExceptionResponse("hey bad things happened sorry"))
|
||||
)
|
||||
|
@ -1,16 +1,16 @@
|
||||
package org.leafygreens.kompendium.playground
|
||||
|
||||
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
|
||||
import org.leafygreens.kompendium.util.KompendiumHttpCodes
|
||||
|
||||
object PlaygroundToC {
|
||||
val testGetWithExamples = MethodInfo.GetInfo<Unit, ExampleResponse>(
|
||||
summary = "Example Parameters",
|
||||
description = "A test for setting parameter examples",
|
||||
responseInfo = ResponseInfo(
|
||||
status = 200,
|
||||
status = HttpStatusCode.OK,
|
||||
description = "nice",
|
||||
examples = mapOf("test" to ExampleResponse(c = "spud"))
|
||||
),
|
||||
@ -27,7 +27,7 @@ object PlaygroundToC {
|
||||
)
|
||||
),
|
||||
responseInfo = ResponseInfo(
|
||||
status = KompendiumHttpCodes.CREATED,
|
||||
status = HttpStatusCode.Created,
|
||||
description = "Congratz you hit da endpoint",
|
||||
examples = mapOf(
|
||||
"Expect This" to ExampleResponse(c = "Hi"),
|
||||
@ -42,7 +42,7 @@ object PlaygroundToC {
|
||||
description = "Test for the getting",
|
||||
tags = setOf("test", "sample", "get"),
|
||||
responseInfo = ResponseInfo(
|
||||
status = KompendiumHttpCodes.OK,
|
||||
status = HttpStatusCode.OK,
|
||||
description = "Returns sample info"
|
||||
)
|
||||
)
|
||||
@ -51,7 +51,7 @@ object PlaygroundToC {
|
||||
description = "testing more",
|
||||
tags = setOf("anotherTest", "sample"),
|
||||
responseInfo = ResponseInfo(
|
||||
status = KompendiumHttpCodes.OK,
|
||||
status = HttpStatusCode.OK,
|
||||
description = "Returns a different sample"
|
||||
)
|
||||
)
|
||||
@ -66,7 +66,7 @@ object PlaygroundToC {
|
||||
description = "Simple request body"
|
||||
),
|
||||
responseInfo = ResponseInfo(
|
||||
status = KompendiumHttpCodes.CREATED,
|
||||
status = HttpStatusCode.Created,
|
||||
description = "Worlds most complex response"
|
||||
)
|
||||
)
|
||||
@ -77,7 +77,7 @@ object PlaygroundToC {
|
||||
description = "Info needed to perform this put request"
|
||||
),
|
||||
responseInfo = ResponseInfo(
|
||||
status = KompendiumHttpCodes.CREATED,
|
||||
status = HttpStatusCode.Created,
|
||||
description = "What we give you when u do the puts"
|
||||
)
|
||||
)
|
||||
@ -85,7 +85,7 @@ object PlaygroundToC {
|
||||
summary = "Test delete endpoint",
|
||||
description = "testing my deletes",
|
||||
responseInfo = ResponseInfo(
|
||||
status = KompendiumHttpCodes.NO_CONTENT,
|
||||
status = HttpStatusCode.NoContent,
|
||||
description = "Signifies that your item was deleted successfully",
|
||||
mediaTypes = emptyList()
|
||||
)
|
||||
@ -95,7 +95,7 @@ object PlaygroundToC {
|
||||
description = "testing more",
|
||||
tags = setOf("anotherTest", "sample"),
|
||||
responseInfo = ResponseInfo(
|
||||
status = KompendiumHttpCodes.OK,
|
||||
status = HttpStatusCode.OK,
|
||||
description = "Returns a different sample"
|
||||
),
|
||||
securitySchemes = setOf("basic")
|
||||
|
Reference in New Issue
Block a user