diff --git a/CHANGELOG.md b/CHANGELOG.md index 45ab7ecd4..18f3a3b07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,16 @@ ## Released +## [4.0.0] - January 21st, 2024 + +### Added + +- All changes from alpha release + +### Changed + +- Enrichments now mirror schema types + ## [4.0.0-alpha] - September 3rd, 2023 ### Added diff --git a/core/src/test/kotlin/io/bkbn/kompendium/core/KompendiumEnrichmentTest.kt b/core/src/test/kotlin/io/bkbn/kompendium/core/KompendiumEnrichmentTest.kt index e86f35140..d9905525e 100644 --- a/core/src/test/kotlin/io/bkbn/kompendium/core/KompendiumEnrichmentTest.kt +++ b/core/src/test/kotlin/io/bkbn/kompendium/core/KompendiumEnrichmentTest.kt @@ -1,8 +1,9 @@ package io.bkbn.kompendium.core -import io.bkbn.kompendium.core.fixtures.TestHelpers +import io.bkbn.kompendium.core.fixtures.TestHelpers.openApiTestAllSerializers import io.bkbn.kompendium.core.util.enrichedComplexGenericType import io.bkbn.kompendium.core.util.enrichedGenericResponse +import io.bkbn.kompendium.core.util.enrichedMap import io.bkbn.kompendium.core.util.enrichedNestedCollection import io.bkbn.kompendium.core.util.enrichedSimpleRequest import io.bkbn.kompendium.core.util.enrichedSimpleResponse @@ -12,24 +13,27 @@ import io.kotest.core.spec.style.DescribeSpec class KompendiumEnrichmentTest : DescribeSpec({ describe("Enrichment") { it("Can enrich a simple request") { - TestHelpers.openApiTestAllSerializers("T0055__enriched_simple_request.json") { enrichedSimpleRequest() } + openApiTestAllSerializers("T0055__enriched_simple_request.json") { enrichedSimpleRequest() } } it("Can enrich a simple response") { - TestHelpers.openApiTestAllSerializers("T0058__enriched_simple_response.json") { enrichedSimpleResponse() } + openApiTestAllSerializers("T0058__enriched_simple_response.json") { enrichedSimpleResponse() } } it("Can enrich a nested collection") { - TestHelpers.openApiTestAllSerializers("T0056__enriched_nested_collection.json") { enrichedNestedCollection() } + openApiTestAllSerializers("T0056__enriched_nested_collection.json") { enrichedNestedCollection() } } it("Can enrich a complex generic type") { - TestHelpers.openApiTestAllSerializers( + openApiTestAllSerializers( "T0057__enriched_complex_generic_type.json" ) { enrichedComplexGenericType() } } it("Can enrich a generic object") { - TestHelpers.openApiTestAllSerializers("T0067__enriched_generic_object.json") { enrichedGenericResponse() } + openApiTestAllSerializers("T0067__enriched_generic_object.json") { enrichedGenericResponse() } } it("Can enrich a top level list type") { - TestHelpers.openApiTestAllSerializers("T0077__enriched_top_level_list.json") { enrichedTopLevelCollection() } + openApiTestAllSerializers("T0077__enriched_top_level_list.json") { enrichedTopLevelCollection() } + } + it("can enrich a map type") { + openApiTestAllSerializers("T0078__enriched_top_level_map.json") { enrichedMap() } } } }) diff --git a/core/src/test/kotlin/io/bkbn/kompendium/core/util/Enrichment.kt b/core/src/test/kotlin/io/bkbn/kompendium/core/util/Enrichment.kt index eaf129fba..d9bf102a1 100644 --- a/core/src/test/kotlin/io/bkbn/kompendium/core/util/Enrichment.kt +++ b/core/src/test/kotlin/io/bkbn/kompendium/core/util/Enrichment.kt @@ -227,3 +227,35 @@ fun Routing.enrichedGenericResponse() { } } } + +fun Routing.enrichedMap() { + route("/example") { + install(NotarizedRoute()) { + get = GetInfo.builder { + summary(TestModules.defaultPathSummary) + description(TestModules.defaultPathDescription) + response { + responseType>( + enrichment = MapEnrichment("blah") { + description = "A nested description" + valueEnrichment = ObjectEnrichment("nested") { + TestSimpleRequest::a { + StringEnrichment("blah-blah-blah") { + description = "A simple description" + } + } + TestSimpleRequest::b { + NumberEnrichment("blah-blah-blah") { + deprecated = true + } + } + } + } + ) + description("A good response") + responseCode(HttpStatusCode.Created) + } + } + } + } +} diff --git a/core/src/test/resources/T0078__enriched_top_level_map.json b/core/src/test/resources/T0078__enriched_top_level_map.json new file mode 100644 index 000000000..bdf7e558f --- /dev/null +++ b/core/src/test/resources/T0078__enriched_top_level_map.json @@ -0,0 +1,103 @@ +{ + "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": { + "/example": { + "get": { + "tags": [], + "summary": "Great Summary!", + "description": "testing more", + "parameters": [], + "responses": { + "201": { + "description": "A good response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Map-String-TestSimpleRequest-blah" + } + } + } + } + }, + "deprecated": false + }, + "parameters": [] + } + }, + "webhooks": {}, + "components": { + "schemas": { + "TestSimpleRequest-nested": { + "type": "object", + "properties": { + "a": { + "type": "string", + "description": "A simple description" + }, + "b": { + "type": "number", + "format": "int32", + "deprecated": true + } + }, + "required": [ + "a", + "b" + ] + }, + "TestSimpleRequest-blah": { + "type": "object", + "properties": { + "a": { + "type": "string", + "description": "A simple description" + }, + "b": { + "type": "number", + "format": "int32", + "deprecated": true + } + }, + "required": [ + "a", + "b" + ] + }, + "Map-String-TestSimpleRequest-blah": { + "additionalProperties": { + "$ref": "#/components/schemas/TestSimpleRequest-blah" + }, + "type": "object" + } + }, + "securitySchemes": {} + }, + "security": [], + "tags": [] +} diff --git a/docs/concepts/enrichment.md b/docs/concepts/enrichment.md index bc28212a9..ba3777af3 100644 --- a/docs/concepts/enrichment.md +++ b/docs/concepts/enrichment.md @@ -1,106 +1,125 @@ -Kompendium allows users to enrich their data types with additional information. This can be done by defining a -`ObjectEnrichment` object and passing it to the `enrichment` parameter of the relevant `requestType` or `responseType`. +Kompendium enables users to enrich their payloads with additional metadata +such as field description, deprecation, and more. + +Enrichments, unlike annotations, are fully decoupled from the implementation of the class +itself. As such, we can not only enable different metadata on the same class in different +areas of the application, we can also reuse the same metadata in different areas, and even +support enrichment of types that you do not own, or types that are not easily annotated, +such as collections and maps. + +A simple enrichment example looks like the following: ```kotlin -data class SimpleData(val a: String, val b: Int? = null) - -val myEnrichment = ObjectEnrichment(id = "simple-enrichment") { - SimpleData::a { - description = "This will update the field description" - } - SimpleData::b { - // Will indicate in the UI that the field will be removed soon - deprecated = true - } -} - -// In your route documentation -fun Routing.enrichedSimpleRequest() { - route("/example") { - install(NotarizedRoute()) { - parameters = TestModules.defaultParams - post = PostInfo.builder { - summary(TestModules.defaultPathSummary) - description(TestModules.defaultPathDescription) - request { - requestType(enrichment = myEnrichment) // Simply attach the enrichment to the request - description("A test request") +post = PostInfo.builder { + summary(TestModules.defaultPathSummary) + description(TestModules.defaultPathDescription) + request { + requestType( + enrichment = ObjectEnrichment("simple") { + TestSimpleRequest::a { + StringEnrichment(id = "simple-enrichment") { + description = "A simple description" + } } - response { - responseCode(HttpStatusCode.Created) - responseType() - description(TestModules.defaultResponseDescription) + TestSimpleRequest::b { + NumberEnrichment(id = "blah-blah-blah") { + deprecated = true + } } } - } + ) + description("A test request") + } + response { + responseCode(HttpStatusCode.Created) + responseType() + description(TestModules.defaultResponseDescription) } } ``` -{% hint style="warning" %} -An enrichment must provide an `id` field that is unique to the data class that is being enriched. This is because -under the hood, Kompendium appends this id to the data class identifier in order to support multiple different -enrichments -on the same data class. +For more information on the various enrichment types, please see the following sections. -If you provide duplicate ids, all but the first enrichment will be ignored, as Kompendium will view that as a cache hit, -and skip analyzing the new enrichment. -{% endhint %} +## Scalar Enrichment -### Nested Enrichments +Currently, Kompendium supports enrichment of the following scalar types: -Enrichments are portable and composable, meaning that we can take an enrichment for a child data class -and apply it inside a parent data class using the `typeEnrichment` property. +- Boolean +- String +- Number + +At the moment, all of these types extend a sealed interface `Enrichment`... as such you cannot provide +enrichments for custom scalars like dates and times. This is a known limitation, and will be addressed +in a future release. + +## Object Enrichment + +Object enrichment is the most common form of enrichment, and is used to enrich a complex type, and +the fields of a class. + +## Collection Enrichment + +Collection enrichment is used to enrich a collection type, and the elements of that collection. ```kotlin -data class ParentData(val a: String, val b: ChildData) -data class ChildData(val c: String, val d: Int? = null) - -val childEnrichment = ObjectEnrichment(id = "child-enrichment") { - ChildData::c { - description = "This will update the field description of field c on child data" +post = PostInfo.builder { + summary(TestModules.defaultPathSummary) + description(TestModules.defaultPathDescription) + request { + requestType( + enrichment = ObjectEnrichment("simple") { + ComplexRequest::tables { + CollectionEnrichment("blah-blah") { + description = "A nested description" + itemEnrichment = ObjectEnrichment("nested") { + NestedComplexItem::name { + StringEnrichment("beleheh") { + description = "A nested description" + } + } + } + } + } + } + ) + description("A test request") } - ChildData::d { - description = "This will update the field description of field d on child data" - } -} - -val parentEnrichment = ObjectEnrichment(id = "parent-enrichment") { - ParentData::a { - description = "This will update the field description" - } - ParentData::b { - description = "This will update the field description of field b on parent data" - typeEnrichment = childEnrichment // Will apply the child enrichment to the internals of field b + response { + responseCode(HttpStatusCode.Created) + responseType() + description(TestModules.defaultResponseDescription) } } ``` -## Available Enrichments +## Map Enrichment -All enrichments support the following properties: +Map enrichment is used to enrich a map type, and the keys and values of that map. -- description -> Provides a reader friendly description of the field in the object -- deprecated -> Indicates that the field is deprecated and should not be used - -### String - -- minLength -> The minimum length of the string -- maxLength -> The maximum length of the string -- pattern -> A regex pattern that the string must match -- contentEncoding -> The encoding of the string -- contentMediaType -> The media type of the string - -### Numbers - -- minimum -> The minimum value of the number -- maximum -> The maximum value of the number -- exclusiveMinimum -> Indicates that the minimum value is exclusive -- exclusiveMaximum -> Indicates that the maximum value is exclusive -- multipleOf -> Indicates that the number must be a multiple of the provided value - -### Arrays - -- minItems -> The minimum number of items in the array -- maxItems -> The maximum number of items in the array -- uniqueItems -> Indicates that the array must contain unique items +```kotlin +get = GetInfo.builder { + summary(TestModules.defaultPathSummary) + description(TestModules.defaultPathDescription) + response { + responseType>( + enrichment = MapEnrichment("blah") { + description = "A nested description" + valueEnrichment = ObjectEnrichment("nested") { + TestSimpleRequest::a { + StringEnrichment("blah-blah-blah") { + description = "A simple description" + } + } + TestSimpleRequest::b { + NumberEnrichment("blah-blah-blah") { + deprecated = true + } + } + } + } + ) + description("A good response") + responseCode(HttpStatusCode.Created) + } +} +``` diff --git a/docs/index.md b/docs/index.md index 2bce8733b..d888caf71 100644 --- a/docs/index.md +++ b/docs/index.md @@ -9,8 +9,9 @@ you to rip out and replace the amazing code you have already written. | 1.X | 1 | 3.0 | | 2.X | 1 | 3.0 | | 3.X | 2 | 3.1 | +| 4.X | 2 | 3.1 | -> These docs are focused solely on Kompendium 3, previous versions should be considered deprecated and no longer +> These docs are focused solely on Kompendium 4, previous versions should be considered deprecated and no longer > maintained # Getting Started diff --git a/docs/playground.md b/docs/playground.md index 5c75f295c..140e85188 100644 --- a/docs/playground.md +++ b/docs/playground.md @@ -1,20 +1,6 @@ The Playground is a module inside the Kompendium repository that provides out of the box examples for a variety of Kompendium features. -At the moment, the following playground applications are - -| Example | Description | -|--------------|------------------------------------------------------------| -| Basic | A minimally viable Kompendium application | -| Auth | Documenting authenticated routes | -| Custom Types | Documenting custom scalars to be used by Kompendium | -| Exceptions | Documenting exception responses | -| Gson | Serialization using Gson instead of the default Kotlinx | -| Hidden Docs | Place your generated documentation behind authorization | -| Jackson | Serialization using Jackson instead of the default KotlinX | -| Locations | Using the Ktor Locations API to define routes | -| Resources | Using the Ktor Resources API to define routes | - -You can find all of the playground +You can find all the playground examples [here](https://github.com/bkbnio/kompendium/tree/main/playground/src/main/kotlin/io/bkbn/kompendium/playground) in the Kompendium repository diff --git a/docs/plugins/notarized_application.md b/docs/plugins/notarized_application.md index c1209fe3b..26b9984ed 100644 --- a/docs/plugins/notarized_application.md +++ b/docs/plugins/notarized_application.md @@ -20,7 +20,8 @@ reference [OpenAPI spec](https://spec.openapis.org/oas/v3.1.0) itself. For public facing APIs, having the default endpoint exposed at `/openapi.json` is totally fine. However, if you need more granular control over the route that exposes the generated schema, you can modify the `openApiJson` config value. -For example, if we want to hide our schema behind a basic auth check with a custom json encoder, we could do the following +For example, if we want to hide our schema behind a basic auth check with a custom json encoder, we could do the +following ```kotlin private fun Application.mainModule() { @@ -30,23 +31,11 @@ private fun Application.mainModule() { specRoute = { spec, routing -> routing { authenticate("basic") { - route("/openapi.json") { - get { - call.response.headers.append("Content-Type", "application/json") - call.respondText { CustomJsonEncoder.encodeToString(spec) } - } + route("/openapi.json") { + get { + call.response.headers.append("Content-Type", "application/json") + call.respondText { CustomJsonEncoder.encodeToString(spec) } } - } - } - } - openApiJson = { - authenticate("basic") { - route("/openapi.json") { - get { - call.respond( - HttpStatusCode.OK, - this@route.application.attributes[KompendiumAttributes.openApiSpec] - ) } } } @@ -86,10 +75,8 @@ This means that we only need to define our custom type once, and then Kompendium application. > While intended for custom scalars, there is nothing stopping you from leveraging custom types to circumvent type -> analysis -> on any class you choose. If you have an alternative method of generating JsonSchema definitions, you could put them -> all -> in this map and effectively prevent Kompendium from having to do any reflection +> analysis on any class you choose. If you have an alternative method of generating JsonSchema definitions, you could +> put them all in this map and effectively prevent Kompendium from having to do any reflection ## Schema Configurator @@ -97,5 +84,5 @@ Out of the box, Kompendium supports KotlinX serialization... however, in order t serialization libraries to use Kompendium, we have provided a `SchemaConfigurator` interface that allows you to configure how Kompendium will generate schema definitions. -For an example of the `SchemaConfigurator` in action, please see the `KotlinxSchemaConfigurator`. This will give you +For an example of the `SchemaConfigurator` in action, please see the `KotlinxSchemaConfigurator`. This will give you a good idea of the additional functionality it can add based on your own serialization needs. diff --git a/gradle.properties b/gradle.properties index 1d4ac3143..d76208066 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ # Kompendium -project.version=4.0.0-alpha +project.version=4.0.0 # Kotlin kotlin.code.style=official diff --git a/playground/src/main/kotlin/io/bkbn/kompendium/playground/HiddenDocsPlayground.kt b/playground/src/main/kotlin/io/bkbn/kompendium/playground/HiddenDocsPlayground.kt index 1603d9925..98f357101 100644 --- a/playground/src/main/kotlin/io/bkbn/kompendium/playground/HiddenDocsPlayground.kt +++ b/playground/src/main/kotlin/io/bkbn/kompendium/playground/HiddenDocsPlayground.kt @@ -1,6 +1,5 @@ package io.bkbn.kompendium.playground -import io.bkbn.kompendium.core.attribute.KompendiumAttributes import io.bkbn.kompendium.core.metadata.GetInfo import io.bkbn.kompendium.core.plugin.NotarizedApplication import io.bkbn.kompendium.core.plugin.NotarizedRoute @@ -27,7 +26,6 @@ import io.ktor.server.engine.embeddedServer import io.ktor.server.plugins.contentnegotiation.ContentNegotiation import io.ktor.server.response.respond import io.ktor.server.routing.Route -import io.ktor.server.routing.application import io.ktor.server.routing.get import io.ktor.server.routing.route import io.ktor.server.routing.routing