From a5376cfa82875a549a4d642864695e81f9e6c905 Mon Sep 17 00:00:00 2001 From: Ryan Brink <5607577+unredundant@users.noreply.github.com> Date: Sat, 5 Nov 2022 16:45:06 -0400 Subject: [PATCH] feat: allow media type overrides (#369) --- CHANGELOG.md | 6 + .../kompendium/core/metadata/RequestInfo.kt | 11 +- .../kompendium/core/metadata/ResponseInfo.kt | 11 +- .../io/bkbn/kompendium/core/util/Helpers.kt | 17 ++- .../io/bkbn/kompendium/core/KompendiumTest.kt | 4 + .../bkbn/kompendium/core/util/TestModules.kt | 22 ++++ .../T0052__override_media_types.json | 123 ++++++++++++++++++ docs/plugins/notarized_route.md | 18 +++ 8 files changed, 201 insertions(+), 11 deletions(-) create mode 100644 core/src/test/resources/T0052__override_media_types.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 233861644..550c43315 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,12 @@ ## Released +## [3.7.0] - November 5th, 2022 + +### Added + +- Allow users to override media type in request and response + ## [3.6.0] - November 5th, 2022 ### Changed diff --git a/core/src/main/kotlin/io/bkbn/kompendium/core/metadata/RequestInfo.kt b/core/src/main/kotlin/io/bkbn/kompendium/core/metadata/RequestInfo.kt index 3d3b73de8..e5062ca67 100644 --- a/core/src/main/kotlin/io/bkbn/kompendium/core/metadata/RequestInfo.kt +++ b/core/src/main/kotlin/io/bkbn/kompendium/core/metadata/RequestInfo.kt @@ -7,7 +7,8 @@ import kotlin.reflect.typeOf class RequestInfo private constructor( val requestType: KType, val description: String, - val examples: Map? + val examples: Map?, + val mediaTypes: Set ) { companion object { @@ -22,6 +23,7 @@ class RequestInfo private constructor( private var requestType: KType? = null private var description: String? = null private var examples: Map? = null + private var mediaTypes: Set? = null fun requestType(t: KType) = apply { this.requestType = t @@ -35,10 +37,15 @@ class RequestInfo private constructor( this.examples = e.toMap().mapValues { (_, v) -> MediaType.Example(v) } } + fun mediaTypes(vararg m: String) = apply { + this.mediaTypes = m.toSet() + } + fun build() = RequestInfo( requestType = requestType ?: error("Request type must be present"), description = description ?: error("Description must be present"), - examples = examples + examples = examples, + mediaTypes = mediaTypes ?: setOf("application/json") ) } } diff --git a/core/src/main/kotlin/io/bkbn/kompendium/core/metadata/ResponseInfo.kt b/core/src/main/kotlin/io/bkbn/kompendium/core/metadata/ResponseInfo.kt index 85c04c096..b0b6426c2 100644 --- a/core/src/main/kotlin/io/bkbn/kompendium/core/metadata/ResponseInfo.kt +++ b/core/src/main/kotlin/io/bkbn/kompendium/core/metadata/ResponseInfo.kt @@ -9,7 +9,8 @@ class ResponseInfo private constructor( val responseCode: HttpStatusCode, val responseType: KType, val description: String, - val examples: Map? + val examples: Map?, + val mediaTypes: Set ) { companion object { @@ -25,6 +26,7 @@ class ResponseInfo private constructor( private var responseType: KType? = null private var description: String? = null private var examples: Map? = null + private var mediaTypes: Set? = null fun responseCode(code: HttpStatusCode) = apply { this.responseCode = code @@ -42,11 +44,16 @@ class ResponseInfo private constructor( this.examples = e.toMap().mapValues { (_, v) -> MediaType.Example(v) } } + fun mediaTypes(vararg m: String) = apply { + this.mediaTypes = m.toSet() + } + fun build() = ResponseInfo( responseCode = responseCode ?: error("You must provide a response code in order to build a Response!"), responseType = responseType ?: error("You must provide a response type in order to build a Response!"), description = description ?: error("You must provide a description in order to build a Response!"), - examples = examples + examples = examples, + mediaTypes = mediaTypes ?: setOf("application/json") ) } } diff --git a/core/src/main/kotlin/io/bkbn/kompendium/core/util/Helpers.kt b/core/src/main/kotlin/io/bkbn/kompendium/core/util/Helpers.kt index c9e15797b..70b62d163 100644 --- a/core/src/main/kotlin/io/bkbn/kompendium/core/util/Helpers.kt +++ b/core/src/main/kotlin/io/bkbn/kompendium/core/util/Helpers.kt @@ -84,7 +84,7 @@ object Helpers { requestBody = when (this) { is MethodInfoWithRequest -> Request( description = this.request.description, - content = this.request.requestType.toReferenceContent(this.request.examples), + content = this.request.requestType.toReferenceContent(this.request.examples, this.request.mediaTypes), required = true ) @@ -93,7 +93,7 @@ object Helpers { responses = mapOf( this.response.responseCode.value to Response( description = this.response.description, - content = this.response.responseType.toReferenceContent(this.response.examples) + content = this.response.responseType.toReferenceContent(this.response.examples, this.response.mediaTypes) ) ).plus(this.errors.toResponseMap()) ) @@ -101,21 +101,24 @@ object Helpers { private fun List.toResponseMap(): Map = associate { error -> error.responseCode.value to Response( description = error.description, - content = error.responseType.toReferenceContent(error.examples) + content = error.responseType.toReferenceContent(error.examples, error.mediaTypes) ) } - private fun KType.toReferenceContent(examples: Map?): Map? = + private fun KType.toReferenceContent( + examples: Map?, + mediaTypes: Set + ): Map? = when (this.classifier as KClass<*>) { Unit::class -> null - else -> mapOf( - "application/json" to MediaType( + else -> mediaTypes.associateWith { + MediaType( schema = if (this.isMarkedNullable) OneOfDefinition( NullableDefinition(), ReferenceDefinition(this.getReferenceSlug()) ) else ReferenceDefinition(this.getReferenceSlug()), examples = examples ) - ) + } } } diff --git a/core/src/test/kotlin/io/bkbn/kompendium/core/KompendiumTest.kt b/core/src/test/kotlin/io/bkbn/kompendium/core/KompendiumTest.kt index e833c0099..9025728b8 100644 --- a/core/src/test/kotlin/io/bkbn/kompendium/core/KompendiumTest.kt +++ b/core/src/test/kotlin/io/bkbn/kompendium/core/KompendiumTest.kt @@ -36,6 +36,7 @@ import io.bkbn.kompendium.core.util.TestModules.nullableEnumField import io.bkbn.kompendium.core.util.TestModules.nullableField import io.bkbn.kompendium.core.util.TestModules.nullableNestedObject import io.bkbn.kompendium.core.util.TestModules.nullableReference +import io.bkbn.kompendium.core.util.TestModules.overrideMediaTypes import io.bkbn.kompendium.core.util.TestModules.polymorphicCollectionResponse import io.bkbn.kompendium.core.util.TestModules.polymorphicException import io.bkbn.kompendium.core.util.TestModules.polymorphicMapResponse @@ -112,6 +113,9 @@ class KompendiumTest : DescribeSpec({ it("Can notarize a route with non-required params") { openApiTestAllSerializers("T0011__non_required_params.json") { nonRequiredParams() } } + it("Can override media types") { + openApiTestAllSerializers("T0052__override_media_types.json") { overrideMediaTypes() } + } } describe("Route Parsing") { it("Can parse a simple path and store it under the expected route") { diff --git a/core/src/test/kotlin/io/bkbn/kompendium/core/util/TestModules.kt b/core/src/test/kotlin/io/bkbn/kompendium/core/util/TestModules.kt index 3a49e5092..680cb8f72 100644 --- a/core/src/test/kotlin/io/bkbn/kompendium/core/util/TestModules.kt +++ b/core/src/test/kotlin/io/bkbn/kompendium/core/util/TestModules.kt @@ -315,6 +315,28 @@ object TestModules { } } + fun Routing.overrideMediaTypes() { + route("/media_types") { + install(NotarizedRoute()) { + put = PutInfo.builder { + summary(defaultPathSummary) + description(defaultPathDescription) + request { + mediaTypes("multipart/form-data", "application/json") + requestType() + description("A cool request") + } + response { + mediaTypes("application/xml") + responseType() + description("A good response") + responseCode(HttpStatusCode.Created) + } + } + } + } + } + fun Routing.simplePathParsing() { route("/this") { route("/is") { diff --git a/core/src/test/resources/T0052__override_media_types.json b/core/src/test/resources/T0052__override_media_types.json new file mode 100644 index 000000000..f67e20e78 --- /dev/null +++ b/core/src/test/resources/T0052__override_media_types.json @@ -0,0 +1,123 @@ +{ + "openapi": "3.1.0", + "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema", + "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": { + "/media_types": { + "put": { + "tags": [], + "summary": "Great Summary!", + "description": "testing more", + "parameters": [], + "requestBody": { + "description": "A cool request", + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/TestRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/TestRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "A good response", + "content": { + "application/xml": { + "schema": { + "$ref": "#/components/schemas/TestResponse" + } + } + } + } + }, + "deprecated": false + }, + "parameters": [] + } + }, + "webhooks": {}, + "components": { + "schemas": { + "TestResponse": { + "type": "object", + "properties": { + "c": { + "type": "string" + } + }, + "required": [ + "c" + ] + }, + "TestRequest": { + "type": "object", + "properties": { + "aaa": { + "items": { + "type": "number", + "format": "int64" + }, + "type": "array" + }, + "b": { + "type": "number", + "format": "double" + }, + "fieldName": { + "$ref": "#/components/schemas/TestNested" + } + }, + "required": [ + "aaa", + "b", + "fieldName" + ] + }, + "TestNested": { + "type": "object", + "properties": { + "nesty": { + "type": "string" + } + }, + "required": [ + "nesty" + ] + } + }, + "securitySchemes": {} + }, + "security": [], + "tags": [] +} diff --git a/docs/plugins/notarized_route.md b/docs/plugins/notarized_route.md index 1dfa95967..77af1959d 100644 --- a/docs/plugins/notarized_route.md +++ b/docs/plugins/notarized_route.md @@ -165,3 +165,21 @@ get = GetInfo.builder { } } ``` + +## Media Types + +By default, Kompendium will set the only media type to "application/json". If you would like to override the media type +for a specific request or response (including errors), you can do so with the `mediaTypes` method + +```kotlin +get = GetInfo.builder { + summary("Get user by id") + description("A very neat endpoint!") + response { + mediaTypes("application/xml") + responseCode(HttpStatusCode.OK) + responseType() + description("Will return whether or not the user is real 😱") + } +} +```