feat: enable creation of explicit parameter examples (#133)

This commit is contained in:
Ryan Brink
2022-01-03 10:34:02 -05:00
committed by GitHub
parent 012db5ad26
commit c6ed261fe4
16 changed files with 168 additions and 15 deletions

View File

@ -3,6 +3,7 @@
## Unreleased
### Added
- Support for HTTP Patch, Head, and Options methods
- Support for including parameter examples via `MethodInfo`
### Changed

View File

@ -3,6 +3,7 @@ package io.bkbn.kompendium.core
import io.bkbn.kompendium.annotations.Param
import io.bkbn.kompendium.core.Kontent.generateKontent
import io.bkbn.kompendium.core.metadata.ExceptionInfo
import io.bkbn.kompendium.core.metadata.ParameterExample
import io.bkbn.kompendium.core.metadata.RequestInfo
import io.bkbn.kompendium.core.metadata.ResponseInfo
import io.bkbn.kompendium.core.metadata.method.MethodInfo
@ -54,7 +55,7 @@ object MethodParser {
operationId = info.operationId,
tags = info.tags,
deprecated = info.deprecated,
parameters = paramType.toParameterSpec(feature),
parameters = paramType.toParameterSpec(info, feature),
responses = parseResponse(responseType, info.responseInfo, feature).plus(parseExceptions(info.canThrow, feature)),
requestBody = when (info) {
is PutInfo<*, *, *> -> requestType.toRequestSpec(info.requestInfo, feature)
@ -161,7 +162,7 @@ object MethodParser {
* @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(feature: Kompendium): List<Parameter> {
private fun KType.toParameterSpec(info: MethodInfo<*, *>, feature: Kompendium): List<Parameter> {
val clazz = classifier as KClass<*>
return clazz.memberProperties.filter { prop ->
prop.findAnnotation<Param>() != null
@ -177,11 +178,21 @@ object MethodParser {
`in` = anny.type.name.lowercase(Locale.getDefault()),
schema = schema.addDefault(defaultValue),
description = schema.description,
required = !prop.returnType.isMarkedNullable && defaultValue == null
required = !prop.returnType.isMarkedNullable && defaultValue == null,
examples = info.parameterExamples.mapToSpec(prop.name)
)
}
}
private fun Set<ParameterExample>.mapToSpec(parameterName: String): Map<String, Parameter.Example>? {
val filtered = filter { it.parameterName == parameterName }
return if (filtered.isEmpty()) {
null
} else {
filtered.associate { it.exampleName to Parameter.Example(it.exampleValue) }
}
}
/**
* Absolutely disgusting reflection to determine if a default value is available for a given property.
* @param clazz to which the property belongs

View File

@ -0,0 +1,3 @@
package io.bkbn.kompendium.core.metadata
data class ParameterExample(val parameterName: String, val exampleName: String, val exampleValue: Any)

View File

@ -1,6 +1,7 @@
package io.bkbn.kompendium.core.metadata.method
import io.bkbn.kompendium.core.metadata.ExceptionInfo
import io.bkbn.kompendium.core.metadata.ParameterExample
import io.bkbn.kompendium.core.metadata.ResponseInfo
data class DeleteInfo<TParam, TResp>(
@ -11,6 +12,6 @@ data class DeleteInfo<TParam, TResp>(
override val deprecated: Boolean = false,
override val securitySchemes: Set<String> = emptySet(),
override val canThrow: Set<ExceptionInfo<*>> = emptySet(),
override val parameterExamples: Map<String, TParam> = emptyMap(),
override val parameterExamples: Set<ParameterExample> = emptySet(),
override val operationId: String? = null
) : MethodInfo<TParam, TResp>

View File

@ -1,6 +1,7 @@
package io.bkbn.kompendium.core.metadata.method
import io.bkbn.kompendium.core.metadata.ExceptionInfo
import io.bkbn.kompendium.core.metadata.ParameterExample
import io.bkbn.kompendium.core.metadata.ResponseInfo
data class GetInfo<TParam, TResp>(
@ -11,6 +12,6 @@ data class GetInfo<TParam, TResp>(
override val deprecated: Boolean = false,
override val securitySchemes: Set<String> = emptySet(),
override val canThrow: Set<ExceptionInfo<*>> = emptySet(),
override val parameterExamples: Map<String, TParam> = emptyMap(),
override val parameterExamples: Set<ParameterExample> = emptySet(),
override val operationId: String? = null
) : MethodInfo<TParam, TResp>

View File

@ -1,6 +1,7 @@
package io.bkbn.kompendium.core.metadata.method
import io.bkbn.kompendium.core.metadata.ExceptionInfo
import io.bkbn.kompendium.core.metadata.ParameterExample
import io.bkbn.kompendium.core.metadata.ResponseInfo
data class HeadInfo<TParam>(
@ -11,6 +12,6 @@ data class HeadInfo<TParam>(
override val deprecated: Boolean = false,
override val securitySchemes: Set<String> = emptySet(),
override val canThrow: Set<ExceptionInfo<*>> = emptySet(),
override val parameterExamples: Map<String, TParam> = emptyMap(),
override val parameterExamples: Set<ParameterExample> = emptySet(),
override val operationId: String? = null
) : MethodInfo<TParam, Unit>

View File

@ -1,6 +1,7 @@
package io.bkbn.kompendium.core.metadata.method
import io.bkbn.kompendium.core.metadata.ExceptionInfo
import io.bkbn.kompendium.core.metadata.ParameterExample
import io.bkbn.kompendium.core.metadata.ResponseInfo
sealed interface MethodInfo<TParam, TResp> {
@ -16,9 +17,13 @@ sealed interface MethodInfo<TParam, TResp> {
val canThrow: Set<ExceptionInfo<*>>
get() = emptySet()
val responseInfo: ResponseInfo<TResp>
// TODO Is this even used anywhere?
val parameterExamples: Map<String, TParam>
get() = emptyMap()
val parameterExamples: Set<ParameterExample>
get() = emptySet()
val operationId: String?
get() = null
}
fun main() {
data class Potato(val a: String)
println(Potato::a)
}

View File

@ -1,6 +1,7 @@
package io.bkbn.kompendium.core.metadata.method
import io.bkbn.kompendium.core.metadata.ExceptionInfo
import io.bkbn.kompendium.core.metadata.ParameterExample
import io.bkbn.kompendium.core.metadata.ResponseInfo
data class OptionsInfo<TParam, TResp>(
@ -11,6 +12,6 @@ data class OptionsInfo<TParam, TResp>(
override val deprecated: Boolean = false,
override val securitySchemes: Set<String> = emptySet(),
override val canThrow: Set<ExceptionInfo<*>> = emptySet(),
override val parameterExamples: Map<String, TParam> = emptyMap(),
override val parameterExamples: Set<ParameterExample> = emptySet(),
override val operationId: String? = null
) : MethodInfo<TParam, TResp>

View File

@ -1,6 +1,7 @@
package io.bkbn.kompendium.core.metadata.method
import io.bkbn.kompendium.core.metadata.ExceptionInfo
import io.bkbn.kompendium.core.metadata.ParameterExample
import io.bkbn.kompendium.core.metadata.RequestInfo
import io.bkbn.kompendium.core.metadata.ResponseInfo
@ -13,6 +14,6 @@ data class PatchInfo<TParam, TReq, TResp>(
override val deprecated: Boolean = false,
override val securitySchemes: Set<String> = emptySet(),
override val canThrow: Set<ExceptionInfo<*>> = emptySet(),
override val parameterExamples: Map<String, TParam> = emptyMap(),
override val parameterExamples: Set<ParameterExample> = emptySet(),
override val operationId: String? = null
) : MethodInfo<TParam, TResp>

View File

@ -1,6 +1,7 @@
package io.bkbn.kompendium.core.metadata.method
import io.bkbn.kompendium.core.metadata.ExceptionInfo
import io.bkbn.kompendium.core.metadata.ParameterExample
import io.bkbn.kompendium.core.metadata.RequestInfo
import io.bkbn.kompendium.core.metadata.ResponseInfo
@ -13,6 +14,6 @@ data class PostInfo<TParam, TReq, TResp>(
override val deprecated: Boolean = false,
override val securitySchemes: Set<String> = emptySet(),
override val canThrow: Set<ExceptionInfo<*>> = emptySet(),
override val parameterExamples: Map<String, TParam> = emptyMap(),
override val parameterExamples: Set<ParameterExample> = emptySet(),
override val operationId: String? = null
) : MethodInfo<TParam, TResp>

View File

@ -1,6 +1,7 @@
package io.bkbn.kompendium.core.metadata.method
import io.bkbn.kompendium.core.metadata.ExceptionInfo
import io.bkbn.kompendium.core.metadata.ParameterExample
import io.bkbn.kompendium.core.metadata.RequestInfo
import io.bkbn.kompendium.core.metadata.ResponseInfo
@ -13,6 +14,6 @@ data class PutInfo<TParam, TReq, TResp>(
override val deprecated: Boolean = false,
override val securitySchemes: Set<String> = emptySet(),
override val canThrow: Set<ExceptionInfo<*>> = emptySet(),
override val parameterExamples: Map<String, TParam> = emptyMap(),
override val parameterExamples: Set<ParameterExample> = emptySet(),
override val operationId: String? = null
) : MethodInfo<TParam, TResp>

View File

@ -8,6 +8,7 @@ import io.bkbn.kompendium.core.util.constrainedDoubleInfo
import io.bkbn.kompendium.core.util.constrainedIntInfo
import io.bkbn.kompendium.core.util.defaultField
import io.bkbn.kompendium.core.util.defaultParameter
import io.bkbn.kompendium.core.util.exampleParams
import io.bkbn.kompendium.core.util.exclusiveMinMax
import io.bkbn.kompendium.core.util.formattedParam
import io.bkbn.kompendium.core.util.freeFormObject
@ -150,6 +151,9 @@ class KompendiumTest : DescribeSpec({
it("Can generate example response and request bodies") {
openApiTest("example_req_and_resp.json") { withExamples() }
}
it("Can describe example parameters") {
openApiTest("example_parameters.json") { exampleParams() }
}
}
describe("Defaults") {
it("Can generate a default parameter values") {

View File

@ -564,3 +564,13 @@ fun Application.minMaxFreeForm() {
}
}
}
fun Application.exampleParams() {
routing {
route("/test/{a}") {
notarizedGet(TestResponseInfo.exampleParams) {
call.respondText { "Hi 🌊" }
}
}
}
}

View File

@ -0,0 +1,97 @@
{
"openapi": "3.0.3",
"info": {
"title": "Test API",
"version": "1.33.7",
"description": "An amazing, fully-ish 😉 generated API spec",
"termsOfService": "https://example.com",
"contact": {
"name": "Homer Simpson",
"url": "https://gph.is/1NPUDiM",
"email": "chunkylover53@aol.com"
},
"license": {
"name": "MIT",
"url": "https://github.com/bkbnio/kompendium/blob/main/LICENSE"
}
},
"servers": [
{
"url": "https://myawesomeapi.com",
"description": "Production instance of my API"
},
{
"url": "https://staging.myawesomeapi.com",
"description": "Where the fun stuff happens"
}
],
"paths": {
"/test/{a}": {
"get": {
"tags": [],
"summary": "param stuff",
"description": "Cool stuff",
"parameters": [
{
"name": "a",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false,
"examples": {
"Testerino": {
"value": "a"
},
"Testerina": {
"value": "b"
}
}
},
{
"name": "aa",
"in": "query",
"schema": {
"format": "int32",
"type": "integer"
},
"required": true,
"deprecated": false,
"examples": {
"Wowza": {
"value": 6
}
}
}
],
"responses": {
"200": {
"description": "A successful endeavor",
"content": {
"application/json": {
"schema": {
"properties": {
"c": {
"type": "string"
}
},
"required": [
"c"
],
"type": "object"
}
}
}
}
},
"deprecated": false
}
}
},
"components": {
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -1,6 +1,7 @@
package io.bkbn.kompendium.core.fixtures
import io.bkbn.kompendium.core.metadata.ExceptionInfo
import io.bkbn.kompendium.core.metadata.ParameterExample
import io.bkbn.kompendium.core.metadata.method.PostInfo
import io.bkbn.kompendium.core.metadata.method.PutInfo
import io.bkbn.kompendium.core.metadata.RequestInfo
@ -266,5 +267,16 @@ object TestResponseInfo {
requestInfo = RequestInfo("cool")
)
val exampleParams = GetInfo<TestParams, TestResponse>(
summary = "param stuff",
description = "Cool stuff",
responseInfo = simpleOkResponse(),
parameterExamples = setOf(
ParameterExample(TestParams::a.name, "Testerino", "a"),
ParameterExample(TestParams::a.name, "Testerina", "b"),
ParameterExample(TestParams::aa.name, "Wowza", 6),
)
)
private fun <T> simpleOkResponse() = ResponseInfo<T>(HttpStatusCode.OK, "A successful endeavor")
}

View File

@ -11,5 +11,8 @@ data class Parameter(
val deprecated: Boolean = false,
val allowEmptyValue: Boolean? = null,
val style: String? = null,
val explode: Boolean? = null
)
val explode: Boolean? = null,
val examples: Map<String, Example>? = null
) {
data class Example(val value: Any)
}