diff --git a/CHANGELOG.md b/CHANGELOG.md index e32d80c30..119984fe7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased ### Added +- Support for HTTP Patch, Head, and Options methods ### Changed diff --git a/gradle.properties b/gradle.properties index a75240f76..114b8f608 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ # Kompendium -project.version=2.0.0-alpha +project.version=2.0.0-beta # Kotlin kotlin.code.style=official # Gradle diff --git a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/Notarized.kt b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/Notarized.kt index 897a7ac87..92bfab351 100644 --- a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/Notarized.kt +++ b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/Notarized.kt @@ -5,6 +5,9 @@ import io.bkbn.kompendium.core.KompendiumPreFlight.methodNotarizationPreFlight import io.bkbn.kompendium.core.MethodParser.parseMethodInfo import io.bkbn.kompendium.core.metadata.method.DeleteInfo import io.bkbn.kompendium.core.metadata.method.GetInfo +import io.bkbn.kompendium.core.metadata.method.HeadInfo +import io.bkbn.kompendium.core.metadata.method.OptionsInfo +import io.bkbn.kompendium.core.metadata.method.PatchInfo import io.bkbn.kompendium.core.metadata.method.PostInfo import io.bkbn.kompendium.core.metadata.method.PutInfo import io.bkbn.kompendium.oas.path.Path @@ -66,7 +69,7 @@ object Notarized { } /** - * Notarization for an HTTP Delete request + * Notarization for an HTTP PUT request * @param TParam The class containing all parameter fields. Each field must be annotated with @[Param] * @param TReq Class detailing the expected API request body * @param TResp Class detailing the expected API response @@ -87,7 +90,28 @@ object Notarized { } /** - * Notarization for an HTTP POST request + * Notarization for an HTTP PATCH request + * @param TParam The class containing all parameter fields. Each field must be annotated with @[Param] + * @param TReq Class detailing the expected API request body + * @param TResp Class detailing the expected API response + * @param info Route metadata + * @param postProcess Adds an optional callback hook to perform manual overrides on the generated [PathOperation] + */ + inline fun Route.notarizedPatch( + info: PatchInfo, + postProcess: (PathOperation) -> PathOperation = { p -> p }, + noinline body: PipelineInterceptor, + ): Route = methodNotarizationPreFlight { paramType, requestType, responseType -> + val feature = this.application.feature(Kompendium) + val path = calculateRoutePath() + feature.config.spec.paths.getOrPut(path) { Path() } + val baseInfo = parseMethodInfo(info, paramType, requestType, responseType, feature) + feature.config.spec.paths[path]?.patch = postProcess(baseInfo) + return method(HttpMethod.Patch) { handle(body) } + } + + /** + * Notarization for an HTTP DELETE request * @param TParam The class containing all parameter fields. Each field must be annotated with @[Param] * @param TResp Class detailing the expected API response * @param info Route metadata @@ -106,6 +130,45 @@ object Notarized { return method(HttpMethod.Delete) { handle(body) } } + /** + * Notarization for an HTTP HEAD request + * @param TParam The class containing all parameter fields. Each field must be annotated with @[Param] + * @param info Route metadata + * @param postProcess Adds an optional callback hook to perform manual overrides on the generated [PathOperation] + */ + inline fun Route.notarizedHead( + info: HeadInfo, + postProcess: (PathOperation) -> PathOperation = { p -> p }, + noinline body: PipelineInterceptor + ): Route = methodNotarizationPreFlight { paramType, requestType, responseType -> + val feature = this.application.feature(Kompendium) + val path = calculateRoutePath() + feature.config.spec.paths.getOrPut(path) { Path() } + val baseInfo = parseMethodInfo(info, paramType, requestType, responseType, feature) + feature.config.spec.paths[path]?.head = postProcess(baseInfo) + return method(HttpMethod.Head) { handle(body) } + } + + /** + * Notarization for an HTTP OPTION request + * @param TParam The class containing all parameter fields. Each field must be annotated with @[Param] + * @param TResp Class detailing the expected API response + * @param info Route metadata + * @param postProcess Adds an optional callback hook to perform manual overrides on the generated [PathOperation] + */ + inline fun Route.notarizedOptions( + info: OptionsInfo, + postProcess: (PathOperation) -> PathOperation = { p -> p }, + noinline body: PipelineInterceptor + ): Route = methodNotarizationPreFlight { paramType, requestType, responseType -> + val feature = this.application.feature(Kompendium) + val path = calculateRoutePath() + feature.config.spec.paths.getOrPut(path) { Path() } + val baseInfo = parseMethodInfo(info, paramType, requestType, responseType, feature) + feature.config.spec.paths[path]?.options = postProcess(baseInfo) + return method(HttpMethod.Options) { handle(body) } + } + /** * Uses the built-in Ktor route path [Route.toString] but cuts out any meta route such as authentication... anything * that matches the RegEx pattern `/\\(.+\\)` diff --git a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/metadata/method/HeadInfo.kt b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/metadata/method/HeadInfo.kt new file mode 100644 index 000000000..c97a45758 --- /dev/null +++ b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/metadata/method/HeadInfo.kt @@ -0,0 +1,16 @@ +package io.bkbn.kompendium.core.metadata.method + +import io.bkbn.kompendium.core.metadata.ExceptionInfo +import io.bkbn.kompendium.core.metadata.ResponseInfo + +data class HeadInfo( + override val responseInfo: ResponseInfo, + override val summary: String, + override val description: String? = null, + override val tags: Set = emptySet(), + override val deprecated: Boolean = false, + override val securitySchemes: Set = emptySet(), + override val canThrow: Set> = emptySet(), + override val parameterExamples: Map = emptyMap(), + override val operationId: String? = null +) : MethodInfo diff --git a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/metadata/method/OptionsInfo.kt b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/metadata/method/OptionsInfo.kt new file mode 100644 index 000000000..55aa7373e --- /dev/null +++ b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/metadata/method/OptionsInfo.kt @@ -0,0 +1,16 @@ +package io.bkbn.kompendium.core.metadata.method + +import io.bkbn.kompendium.core.metadata.ExceptionInfo +import io.bkbn.kompendium.core.metadata.ResponseInfo + +data class OptionsInfo( + override val responseInfo: ResponseInfo, + override val summary: String, + override val description: String? = null, + override val tags: Set = emptySet(), + override val deprecated: Boolean = false, + override val securitySchemes: Set = emptySet(), + override val canThrow: Set> = emptySet(), + override val parameterExamples: Map = emptyMap(), + override val operationId: String? = null +) : MethodInfo diff --git a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/metadata/method/PatchInfo.kt b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/metadata/method/PatchInfo.kt new file mode 100644 index 000000000..ea7759de5 --- /dev/null +++ b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/metadata/method/PatchInfo.kt @@ -0,0 +1,18 @@ +package io.bkbn.kompendium.core.metadata.method + +import io.bkbn.kompendium.core.metadata.ExceptionInfo +import io.bkbn.kompendium.core.metadata.RequestInfo +import io.bkbn.kompendium.core.metadata.ResponseInfo + +data class PatchInfo( + val requestInfo: RequestInfo?, + override val responseInfo: ResponseInfo, + override val summary: String, + override val description: String? = null, + override val tags: Set = emptySet(), + override val deprecated: Boolean = false, + override val securitySchemes: Set = emptySet(), + override val canThrow: Set> = emptySet(), + override val parameterExamples: Map = emptyMap(), + override val operationId: String? = null +) : MethodInfo diff --git a/kompendium-core/src/test/kotlin/io/bkbn/kompendium/core/KompendiumTest.kt b/kompendium-core/src/test/kotlin/io/bkbn/kompendium/core/KompendiumTest.kt index e4475368d..a711dab54 100644 --- a/kompendium-core/src/test/kotlin/io/bkbn/kompendium/core/KompendiumTest.kt +++ b/kompendium-core/src/test/kotlin/io/bkbn/kompendium/core/KompendiumTest.kt @@ -27,6 +27,9 @@ import io.bkbn.kompendium.core.util.notarizedGetWithGenericErrorResponse import io.bkbn.kompendium.core.util.notarizedGetWithMultipleThrowables import io.bkbn.kompendium.core.util.notarizedGetWithNotarizedException import io.bkbn.kompendium.core.util.notarizedGetWithPolymorphicErrorResponse +import io.bkbn.kompendium.core.util.notarizedHeadModule +import io.bkbn.kompendium.core.util.notarizedOptionsModule +import io.bkbn.kompendium.core.util.notarizedPatchModule import io.bkbn.kompendium.core.util.notarizedPostModule import io.bkbn.kompendium.core.util.notarizedPutModule import io.bkbn.kompendium.core.util.nullableField @@ -55,56 +58,53 @@ import io.ktor.http.HttpStatusCode class KompendiumTest : DescribeSpec({ describe("Notarized Open API Metadata Tests") { it("Can notarize a get request") { - // act openApiTest("notarized_get.json") { notarizedGetModule() } } it("Can notarize a post request") { - // act openApiTest("notarized_post.json") { notarizedPostModule() } } it("Can notarize a put request") { - // act openApiTest("notarized_put.json") { notarizedPutModule() } } it("Can notarize a delete request") { - // act openApiTest("notarized_delete.json") { notarizedDeleteModule() } } + it("Can notarize a patch request") { + openApiTest("notarized_patch.json") { notarizedPatchModule() } + } + it("Can notarize a head request") { + openApiTest("notarized_head.json") { notarizedHeadModule() } + } + it("Can notarize an options request") { + openApiTest("notarized_options.json") { notarizedOptionsModule() } + } it("Can notarize a complex type") { - // act openApiTest("complex_type.json") { complexType() } } it("Can notarize primitives") { - // act openApiTest("notarized_primitives.json") { primitives() } } it("Can notarize a top level list response") { - // act openApiTest("response_list.json") { returnsList() } } it("Can notarize a route with non-required params") { - // act openApiTest("non_required_params.json") { nonRequiredParamsGet() } } } describe("Notarized Ktor Functionality Tests") { it("Can notarized a get request and return the expected result") { - // act apiFunctionalityTest("hey dude ‼️ congratz on the get request") { notarizedGetModule() } } it("Can notarize a post request and return the expected result") { - // act apiFunctionalityTest( "hey dude ✌️ congratz on the post request", httpMethod = HttpMethod.Post ) { notarizedPostModule() } } it("Can notarize a put request and return the expected result") { - // act apiFunctionalityTest("hey pal 🌝 whatcha doin' here?", httpMethod = HttpMethod.Put) { notarizedPutModule() } } it("Can notarize a delete request and return the expected result") { - // act apiFunctionalityTest( null, httpMethod = HttpMethod.Delete, @@ -112,59 +112,47 @@ class KompendiumTest : DescribeSpec({ ) { notarizedDeleteModule() } } it("Can notarize the root route and return the expected result") { - // act apiFunctionalityTest("☎️🏠🌲", "/") { rootModule() } } it("Can notarize a trailing slash route and return the expected result") { - // act apiFunctionalityTest("🙀👾", "/test/") { trailingSlash() } } } describe("Route Parsing") { it("Can parse a simple path and store it under the expected route") { - // act openApiTest("path_parser.json") { pathParsingTestModule() } } it("Can notarize the root route") { - // act openApiTest("root_route.json") { rootModule() } } it("Can notarize a route under the root module without appending trailing slash") { - // act openApiTest("nested_under_root.json") { nestedUnderRootModule() } } it("Can notarize a route with a trailing slash") { - // act openApiTest("trailing_slash.json") { trailingSlash() } } } describe("Exceptions") { it("Can add an exception status code to a response") { - // act openApiTest("notarized_get_with_exception_response.json") { notarizedGetWithNotarizedException() } } it("Can support multiple response codes") { - // act openApiTest("notarized_get_with_multiple_exception_responses.json") { notarizedGetWithMultipleThrowables() } } it("Can add a polymorphic exception response") { - // act openApiTest("polymorphic_error_status_codes.json") { notarizedGetWithPolymorphicErrorResponse() } } it("Can add a generic exception response") { - // act openApiTest("generic_exception.json") { notarizedGetWithGenericErrorResponse() } } } describe("Examples") { it("Can generate example response and request bodies") { - // act openApiTest("example_req_and_resp.json") { withExamples() } } } describe("Defaults") { it("Can generate a default parameter values") { - // act openApiTest("query_with_default_parameter.json") { withDefaultParameter() } } } @@ -184,49 +172,38 @@ class KompendiumTest : DescribeSpec({ } describe("Polymorphism and Generics") { it("can generate a polymorphic response type") { - // act openApiTest("polymorphic_response.json") { polymorphicResponse() } } it("Can generate a collection with polymorphic response type") { - // act openApiTest("polymorphic_list_response.json") { polymorphicCollectionResponse() } } it("Can generate a map with a polymorphic response type") { - // act openApiTest("polymorphic_map_response.json") { polymorphicMapResponse() } } it("Can generate a polymorphic response from a sealed interface") { - // act openApiTest("sealed_interface_response.json") { polymorphicInterfaceResponse() } } it("Can generate a response type with a generic type") { - // act openApiTest("generic_response.json") { simpleGenericResponse() } } it("Can generate a polymorphic response type with generics") { - // act openApiTest("polymorphic_response_with_generics.json") { genericPolymorphicResponse() } } it("Can handle an absolutely psycho inheritance test") { - // act openApiTest("crazy_polymorphic_example.json") { genericPolymorphicResponseMultipleImpls() } } } describe("Miscellaneous") { it("Can generate the necessary ReDoc home page") { - // act apiFunctionalityTest(getFileSnapshot("redoc.html"), "/docs") { returnsList() } } it("Can add an operation id to a notarized route") { - // act openApiTest("notarized_get_with_operation_id.json") { withOperationId() } } it("Can add an undeclared field") { - // act openApiTest("undeclared_field.json") { undeclaredType() } } it("Can add a custom header parameter with a name override") { - // act openApiTest("override_parameter_name.json") { headerParameter() } } it("Can override field values via annotation") { diff --git a/kompendium-core/src/test/kotlin/io/bkbn/kompendium/core/util/TestModules.kt b/kompendium-core/src/test/kotlin/io/bkbn/kompendium/core/util/TestModules.kt index 67c89d3e5..f1d0467a6 100644 --- a/kompendium-core/src/test/kotlin/io/bkbn/kompendium/core/util/TestModules.kt +++ b/kompendium-core/src/test/kotlin/io/bkbn/kompendium/core/util/TestModules.kt @@ -2,6 +2,9 @@ package io.bkbn.kompendium.core.util import io.bkbn.kompendium.core.Notarized.notarizedDelete import io.bkbn.kompendium.core.Notarized.notarizedGet +import io.bkbn.kompendium.core.Notarized.notarizedHead +import io.bkbn.kompendium.core.Notarized.notarizedOptions +import io.bkbn.kompendium.core.Notarized.notarizedPatch import io.bkbn.kompendium.core.Notarized.notarizedPost import io.bkbn.kompendium.core.Notarized.notarizedPut import io.bkbn.kompendium.core.fixtures.Bibbity @@ -105,6 +108,36 @@ fun Application.notarizedDeleteModule() { } } +fun Application.notarizedPatchModule() { + routing { + route("/test") { + notarizedPatch(TestResponseInfo.testPatchInfo) { + call.respondText { "hey dude ✌️ congratz on the patch request" } + } + } + } +} + +fun Application.notarizedHeadModule() { + routing { + route("/test") { + notarizedHead(TestResponseInfo.testHeadInfo) { + call.response.status(HttpStatusCode.OK) + } + } + } +} + +fun Application.notarizedOptionsModule() { + routing { + route("/test") { + notarizedOptions(TestResponseInfo.testOptionsInfo) { + call.response.status(HttpStatusCode.OK) + } + } + } +} + fun Application.notarizedPutModule() { routing { route("/test") { @@ -245,12 +278,12 @@ fun Application.withDefaultParameter() { } } -fun Application.withOperationId(){ +fun Application.withOperationId() { routing { route("/test") { notarizedGet( info = TestResponseInfo.testGetInfo.copy(operationId = "getTest") - ){ + ) { call.respond(HttpStatusCode.OK) } } diff --git a/kompendium-core/src/test/resources/notarized_head.json b/kompendium-core/src/test/resources/notarized_head.json new file mode 100644 index 000000000..0090603eb --- /dev/null +++ b/kompendium-core/src/test/resources/notarized_head.json @@ -0,0 +1,49 @@ +{ + "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": { + "head": { + "tags": [], + "summary": "Test head endpoint", + "description": "head test 💀", + "parameters": [], + "responses": { + "200": { + "description": "great!" + } + }, + "deprecated": false + } + } + }, + "components": { + "securitySchemes": {} + }, + "security": [], + "tags": [] +} diff --git a/kompendium-core/src/test/resources/notarized_options.json b/kompendium-core/src/test/resources/notarized_options.json new file mode 100644 index 000000000..76c220088 --- /dev/null +++ b/kompendium-core/src/test/resources/notarized_options.json @@ -0,0 +1,84 @@ +{ + "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": { + "options": { + "tags": [], + "summary": "Test options", + "description": "endpoint of options", + "parameters": [ + { + "name": "a", + "in": "path", + "schema": { + "type": "string" + }, + "required": true, + "deprecated": false + }, + { + "name": "aa", + "in": "query", + "schema": { + "format": "int32", + "type": "integer" + }, + "required": true, + "deprecated": false + } + ], + "responses": { + "200": { + "description": "nice", + "content": { + "application/json": { + "schema": { + "properties": { + "c": { + "type": "string" + } + }, + "required": [ + "c" + ], + "type": "object" + } + } + } + } + }, + "deprecated": false + } + } + }, + "components": { + "securitySchemes": {} + }, + "security": [], + "tags": [] +} diff --git a/kompendium-core/src/test/resources/notarized_patch.json b/kompendium-core/src/test/resources/notarized_patch.json new file mode 100644 index 000000000..79f7bbfad --- /dev/null +++ b/kompendium-core/src/test/resources/notarized_patch.json @@ -0,0 +1,64 @@ +{ + "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": { + "patch": { + "tags": [], + "summary": "Test patch endpoint", + "description": "patch your tests here!", + "parameters": [], + "responses": { + "201": { + "description": "A Successful Endeavor", + "content": { + "application/json": { + "schema": { + "properties": { + "c": { + "type": "string" + } + }, + "required": [ + "c" + ], + "type": "object" + } + } + } + } + }, + "deprecated": false + } + } + }, + "components": { + "securitySchemes": {} + }, + "security": [], + "tags": [] +} diff --git a/kompendium-core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/TestResponseInfo.kt b/kompendium-core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/TestResponseInfo.kt index 44df6edc9..9ecb89ab7 100644 --- a/kompendium-core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/TestResponseInfo.kt +++ b/kompendium-core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/TestResponseInfo.kt @@ -7,6 +7,9 @@ import io.bkbn.kompendium.core.metadata.RequestInfo import io.bkbn.kompendium.core.metadata.ResponseInfo import io.bkbn.kompendium.core.metadata.method.DeleteInfo import io.bkbn.kompendium.core.metadata.method.GetInfo +import io.bkbn.kompendium.core.metadata.method.HeadInfo +import io.bkbn.kompendium.core.metadata.method.OptionsInfo +import io.bkbn.kompendium.core.metadata.method.PatchInfo import io.ktor.http.HttpStatusCode import kotlin.reflect.typeOf @@ -15,6 +18,7 @@ object TestResponseInfo { private val testGetListResponse = ResponseInfo>(HttpStatusCode.OK, "A Successful List-y Endeavor") private val testPostResponse = ResponseInfo(HttpStatusCode.Created, "A Successful Endeavor") + private val testPatchResponse = ResponseInfo(HttpStatusCode.Created, "A Successful Endeavor") private val testPostResponseAgain = ResponseInfo(HttpStatusCode.Created, "A Successful Endeavor") private val testDeleteResponse = ResponseInfo(HttpStatusCode.NoContent, "A Successful Endeavor", mediaTypes = emptyList()) @@ -75,6 +79,22 @@ object TestResponseInfo { responseInfo = testPostResponse, requestInfo = complexRequest ) + val testPatchInfo = PatchInfo( + summary = "Test patch endpoint", + description = "patch your tests here!", + responseInfo = testPatchResponse, + requestInfo = testRequest + ) + val testHeadInfo = HeadInfo( + summary = "Test head endpoint", + description = "head test 💀", + responseInfo = ResponseInfo(HttpStatusCode.OK, "great!") + ) + val testOptionsInfo = OptionsInfo( + summary = "Test options", + description = "endpoint of options", + responseInfo = ResponseInfo(HttpStatusCode.OK, "nice") + ) val testPutInfoAlso = PutInfo( summary = "Test put endpoint", description = "Put your tests here!",