From ca1f6326654b690d2490c97538bdafb533a767d6 Mon Sep 17 00:00:00 2001 From: Bronson Date: Thu, 6 Apr 2023 15:47:48 -0400 Subject: [PATCH] feat: Introduce Support for Response Headers (#446) --- CHANGELOG.md | 2 + .../kompendium/core/metadata/ResponseInfo.kt | 12 +- .../io/bkbn/kompendium/core/util/Helpers.kt | 2 + .../io/bkbn/kompendium/core/KompendiumTest.kt | 4 + .../kompendium/core/util/NotarizedOpenApi.kt | 34 ++++++ ...__notarized_get_with_response_headers.json | 110 ++++++++++++++++++ 6 files changed, 162 insertions(+), 2 deletions(-) create mode 100644 core/src/test/resources/T0066__notarized_get_with_response_headers.json diff --git a/CHANGELOG.md b/CHANGELOG.md index fd05fa3f0..bafd6e49a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Add support for response headers + ### Added ### Changed 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 28380448f..f4a970e43 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 @@ -1,6 +1,7 @@ package io.bkbn.kompendium.core.metadata import io.bkbn.kompendium.enrichment.TypeEnrichment +import io.bkbn.kompendium.oas.payload.Header import io.bkbn.kompendium.oas.payload.MediaType import io.ktor.http.HttpStatusCode import kotlin.reflect.KType @@ -12,7 +13,8 @@ class ResponseInfo private constructor( val typeEnrichment: TypeEnrichment<*>?, val description: String, val examples: Map?, - val mediaTypes: Set + val mediaTypes: Set, + val responseHeaders: Map? ) { companion object { @@ -30,6 +32,11 @@ class ResponseInfo private constructor( private var description: String? = null private var examples: Map? = null private var mediaTypes: Set? = null + private var responseHeaders: Map? = null + + fun responseHeaders(headers: Map) = apply { + this.responseHeaders = headers + } fun responseCode(code: HttpStatusCode) = apply { this.responseCode = code @@ -64,7 +71,8 @@ class ResponseInfo private constructor( description = description ?: error("You must provide a description in order to build a Response!"), typeEnrichment = typeEnrichment, examples = examples, - mediaTypes = mediaTypes ?: setOf("application/json") + mediaTypes = mediaTypes ?: setOf("application/json"), + responseHeaders = responseHeaders ) } } 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 a080b656c..4075fe67c 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 @@ -140,6 +140,7 @@ object Helpers { responses = mapOf( this.response.responseCode.value to Response( description = this.response.description, + headers = this.response.responseHeaders, content = this.response.responseType.toReferenceContent( examples = this.response.examples, mediaTypes = this.response.mediaTypes, @@ -152,6 +153,7 @@ object Helpers { private fun List.toResponseMap(): Map = associate { error -> error.responseCode.value to Response( description = error.description, + headers = error.responseHeaders, content = error.responseType.toReferenceContent( examples = error.examples, mediaTypes = error.mediaTypes, 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 d15632440..1a453b544 100644 --- a/core/src/test/kotlin/io/bkbn/kompendium/core/KompendiumTest.kt +++ b/core/src/test/kotlin/io/bkbn/kompendium/core/KompendiumTest.kt @@ -52,6 +52,7 @@ import io.bkbn.kompendium.core.util.postNoReqBody import io.bkbn.kompendium.core.util.primitives import io.bkbn.kompendium.core.util.reqRespExamples import io.bkbn.kompendium.core.util.requiredParams +import io.bkbn.kompendium.core.util.responseHeaders import io.bkbn.kompendium.core.util.returnsList import io.bkbn.kompendium.core.util.rootRoute import io.bkbn.kompendium.core.util.samePathDifferentMethodsAndAuth @@ -133,6 +134,9 @@ class KompendiumTest : DescribeSpec({ it("Can support a post request with no request body") { openApiTestAllSerializers("T0065__post_no_req_body.json") { postNoReqBody() } } + it("Can notarize a route with response headers") { + openApiTestAllSerializers("T0066__notarized_get_with_response_headers.json") { responseHeaders() } + } } 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/NotarizedOpenApi.kt b/core/src/test/kotlin/io/bkbn/kompendium/core/util/NotarizedOpenApi.kt index 376da49e7..9d6b2dfe4 100644 --- a/core/src/test/kotlin/io/bkbn/kompendium/core/util/NotarizedOpenApi.kt +++ b/core/src/test/kotlin/io/bkbn/kompendium/core/util/NotarizedOpenApi.kt @@ -20,7 +20,9 @@ import io.bkbn.kompendium.core.util.TestModules.defaultPathSummary import io.bkbn.kompendium.core.util.TestModules.defaultResponseDescription import io.bkbn.kompendium.core.util.TestModules.rootPath import io.bkbn.kompendium.json.schema.definition.TypeDefinition +import io.bkbn.kompendium.oas.payload.Header import io.bkbn.kompendium.oas.payload.Parameter +import io.ktor.http.HttpHeaders import io.ktor.http.HttpStatusCode import io.ktor.server.application.call import io.ktor.server.application.install @@ -56,6 +58,38 @@ fun Routing.notarizedGet() { } } +fun Routing.responseHeaders() { + route(defaultPath) { + install(NotarizedRoute()) { + parameters = defaultParams + get = GetInfo.builder { + response { + responseCode(HttpStatusCode.OK) + responseType() + description(defaultResponseDescription) + responseHeaders( + mapOf( + HttpHeaders.ETag to Header( + TypeDefinition.STRING, + "https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag" + ), + HttpHeaders.LastModified to Header( + TypeDefinition.STRING, + "https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Last-Modified" + ), + ) + ) + } + summary(defaultPathSummary) + description(defaultPathDescription) + } + } + get { + call.respondText { "hey dude ‼️ congrats on the get request" } + } + } +} + fun Routing.notarizedPost() { route(defaultPath) { install(NotarizedRoute()) { diff --git a/core/src/test/resources/T0066__notarized_get_with_response_headers.json b/core/src/test/resources/T0066__notarized_get_with_response_headers.json new file mode 100644 index 000000000..a7e6d991a --- /dev/null +++ b/core/src/test/resources/T0066__notarized_get_with_response_headers.json @@ -0,0 +1,110 @@ +{ + "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": { + "/test/{a}": { + "get": { + "tags": [], + "summary": "Great Summary!", + "description": "testing more", + "parameters": [], + "responses": { + "200": { + "description": "A Successful Endeavor", + "headers": { + "ETag": { + "schema": { + "type": "string" + }, + "description": "https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag", + "required": true, + "deprecated": false + }, + "Last-Modified": { + "schema": { + "type": "string" + }, + "description": "https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Last-Modified", + "required": true, + "deprecated": false + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TestResponse" + } + } + } + } + }, + "deprecated": false + }, + "parameters": [ + { + "name": "a", + "in": "path", + "schema": { + "type": "string" + }, + "required": true, + "deprecated": false + }, + { + "name": "aa", + "in": "query", + "schema": { + "type": "number", + "format": "int32" + }, + "required": true, + "deprecated": false + } + ] + } + }, + "webhooks": {}, + "components": { + "schemas": { + "TestResponse": { + "type": "object", + "properties": { + "c": { + "type": "string" + } + }, + "required": [ + "c" + ] + } + }, + "securitySchemes": {} + }, + "security": [], + "tags": [] +}