diff --git a/.gitbook.yaml b/.gitbook.yaml new file mode 100644 index 0000000000..e454be0e25 --- /dev/null +++ b/.gitbook.yaml @@ -0,0 +1 @@ +root: ./docs/ diff --git a/README.md b/README.md index d0a8383836..f6e29beed6 100644 --- a/README.md +++ b/README.md @@ -2,55 +2,4 @@ [![version](https://img.shields.io/maven-central/v/io.bkbn/kompendium-core?style=flat-square)](https://search.maven.org/search?q=io.bkbn%20kompendium) -## Table of Contents - -- [What Is Kompendium](#what-is-kompendium) -- [How to Install](#how-to-install) -- [Local Development](#local-development) -- [The Playground](#the-playground) - -## What is Kompendium - -Kompendium is intended to be a minimally invasive OpenApi Specification generator for Ktor. Minimally invasive meaning -that users will use only Ktor native functions when implementing their API, and will supplement with Kompendium code in -order to generate the appropriate spec. - -### Compatability - -| Kompendium | Ktor | OpenAPI | -|------------|------|---------| -| 1.X | 1 | 3.0 | -| 2.X | 1 | 3.0 | -| 3.X | 2 | 3.1 | - -## How to install - -Kompendium publishes all releases to Maven Central. As such, using the release versions of `Kompendium` is as simple as -declaring it as an implementation dependency in your `build.gradle.kts` - -```kotlin -repositories { - mavenCentral() -} - -dependencies { - implementation("io.bkbn:kompendium-core:latest.release") -} -``` - -In addition to publishing releases to Maven Central, a snapshot version gets published to GitHub Packages on every merge -to `main`. These can be consumed by adding the repository to your gradle build file. Instructions can be -found [here](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-gradle-registry#using-a-published-package) - -## Local Development - -Kompendium should run locally right out of the box, no configuration necessary (assuming you have JDK 11+ installed). -New features can be built locally and published to your local maven repository with the `./gradlew publishToMavenLocal` -command! - -## The Playground - -This repo contains a `playground` module that contains a number of working examples showcasing the capabilities of -Kompendium. - -Feel free to check it out, or even create your own example! +Documentation is stored in the `docs` folder and is hosted [here](https://bkbn.gitbook.io/kompendium) diff --git a/book.json b/book.json deleted file mode 100644 index 95e6692e89..0000000000 --- a/book.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "root": "./docs" -} diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md new file mode 100644 index 0000000000..e10eadc88f --- /dev/null +++ b/docs/SUMMARY.md @@ -0,0 +1,8 @@ +# Summary + +* [Introduction](index.md) +* [Plugins](plugins/index.md) + * [Notarized Application](plugins/notarized_application.md) + * [Notarized Route](plugins/notarized_route.md) + * [Notarized Locations](plugins/notarized_locations.md) +* [The Playground](playground.md) diff --git a/docs/index.md b/docs/index.md index 0db584c3a1..f87df8cba7 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,3 +1,101 @@ -# Kompendium +Kompendium is intended to be a non-invasive OpenAPI spec generator for [Ktor](https://ktor.io) APIs. By operating +entirely through Ktor's plugin architecture, Kompendium allows you to incrementally document your API without requiring +you to rip out and replace the amazing code you have already written. -Hi :) +# Compatibility + +| Kompendium | Ktor | OpenAPI | +|------------|------|---------| +| 1.X | 1 | 3.0 | +| 2.X | 1 | 3.0 | +| 3.X | 2 | 3.1 | + +> These docs are focused solely on Kompendium 3, previous versions should be considered deprecated and no longer +> maintained + +# Getting Started + +## Adding the Artifact + +All Kompendium artifacts are published to Maven Central. Most Kompendium users will only need to import the core +dependency + +```kotlin +dependencies { + // other dependencies... + implementation("io.bkbn:kompendium-core:latest.release") +} +``` + +## Notarizing a Ktor Application + +Once we have added the dependencies, installed the `NotarizedApplication` plugin. This is an application-level +Ktor plugin that is used to instantiate and configure Kompendium. Your OpenAPI spec metadata will go here, along with +custom type overrides (typically useful for custom scalars such as dates and times), along with other configurations. + +```kotlin +private fun Application.mainModule() { + install(NotarizedApplication()) { + spec = OpenApiSpec( + // ... + ) + } +} +``` + +At this point, you will have a valid OpenAPI specification generated at runtime, which can be accessed by default +at the `/openapi.json` path of your api. + +For more detail on the `NotarizedApplication` plugin, please see the [docs](./plugins/notarized_application.md) + +## Notarizing a Ktor Route + +Once you have notarized your application, you can begin to notarize individual routes using the `NotarizedRoute` plugin. +This is a route-level Ktor plugin that is used to configure the documentation for a specific endpoint of your API. The +route documentation will be piped back to the application-level plugin, and will be automatically injected into the +OpenApi specification. + +Setting up documentation on a route is easiest achieved by creating an extension function on the Ktor `Route` class + +```kotlin +private fun Route.documentation() { + install(NotarizedRoute()) { + parameters = listOf( + Parameter( + name = "id", + `in` = Parameter.Location.path, + schema = TypeDefinition.STRING + ) + ) + get = GetInfo.builder { + summary("Get user by id") + description("A very neat endpoint!") + response { + responseCode(HttpStatusCode.OK) + responseType() + description("Will return whether or not the user is real 😱") + } + } + } +} +``` + +Once you have created your documentation function, it can be attached to the route simply by calling it at the desired +path. + +```kotlin +private fun Application.mainModule() { + // ... + routing { + // ... + route("/{id}") { + documentation() + get { + // ... + } + } + } +} +``` + +For more details on the `NotarizedRoute` plugin, please see the [docs](./plugins/notarized_route.md) diff --git a/docs/playground.md b/docs/playground.md new file mode 100644 index 0000000000..69d85e7866 --- /dev/null +++ b/docs/playground.md @@ -0,0 +1,19 @@ +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 | + +You can find all of 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/index.md b/docs/plugins/index.md new file mode 100644 index 0000000000..05eaec31c9 --- /dev/null +++ b/docs/plugins/index.md @@ -0,0 +1,11 @@ +Plugins are the lifeblood of Kompendium. + +It starts with the `NotarizedApplication`, where Kompendium is instantiated and attached to the API. This is where spec +metadata is defined, custom types are defined, and more. + +From there, a `NotarizedRoute` plugin is attached to each route you wish to document. This allows API documentation to +be an iterative process. Each route you notarize will be picked up and injected into the OpenAPI spec that Kompendium +generates for you. + +Finally, there is the `NotarizedLocations` plugin that allows you to leverage and document your usage of the +Ktor [Locations](https://ktor.io/docs/locations.html) API. diff --git a/docs/plugins/notarized_application.md b/docs/plugins/notarized_application.md new file mode 100644 index 0000000000..e752a279d2 --- /dev/null +++ b/docs/plugins/notarized_application.md @@ -0,0 +1,105 @@ +The `NotarizedApplication` plugin sits at the center of the entire Kompendium setup. It is a pre-requisite to +installing any other Kompendium plugins. + +# Configuration + +Very little configuration is needed for a basic documentation setup, but +several configuration options are available that allow you to modify Kompendium to fit your needs. + +## Spec + +This is where you will define the server metadata that lives outside the scope of any specific route. For +full information, you can inspect the `OpenApiSpec` data class, and of course +reference [OpenAPI spec](https://spec.openapis.org/oas/v3.1.0) itself. + +> ⚠️ Please note, the `path` field of the `OpenApiSpec` is intended to be filled in by `NotarizedRoute` plugin +> definitions. Writing custom paths manually could lead to unexpected behavior + +## Custom Routing + +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, we could do the following + +```kotlin +private fun Application.mainModule() { + // Install content negotiation, auth, etc... + install(NotarizedApplication()) { + // ... + openApiJson = { + authenticate("basic") { + route("/openapi.json") { + get { + call.respond( + HttpStatusCode.OK, + this@route.application.attributes[KompendiumAttributes.openApiSpec] + ) + } + } + } + } + } +} +``` + +## Custom Types + +Kompendium is _really_ good at converting simple scalar and complex objects into JsonSchema compliant specs. However, +there is a subset of values that cause it trouble. These are most commonly classes that produce "complex scalars", +such as dates and times, along with object representations of scalars such as `BigInteger`. + +In situations like this, you will need to define a map of custom types to JsonSchema definitions that Kompendium can use +to short-circuit its type analysis. + +For example, say we would like to serialize `kotlinx.datetime.Instant` entities as a field in our response objects. We +would need to add it as a custom type. + +```kotlin +private fun Application.mainModule() { + // ... + install(NotarizedApplication()) { + spec = baseSpec + customTypes = mapOf( + typeOf() to TypeDefinition(type = "string", format = "date-time") + ) + } +} +``` + +Doing this will save it in a cache that our `NotarizedRoute` plugin definitions will check from prior to attempting to +perform type inspection. + +This means that we only need to define our custom type once, and then Kompendium will reuse it across the entire +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 + +## Schema Configurator + +The `SchemaConfigurator` is an interface that allows users to bridge the gap between Kompendium serialization and custom +serialization strategies that the serializer they are using for their API. For example, if you are using KotlinX +serialization in order to convert kotlin fields from camel case to snake case, you could leverage +the `KotlinXSchemaConfigurator` in order to instruct Kompendium on how to serialize values properly. + +```kotlin +private fun Application.mainModule() { + install(ContentNegotiation) { + json(Json { + serializersModule = KompendiumSerializersModule.module + encodeDefaults = true + explicitNulls = false + }) + } + install(NotarizedApplication()) { + spec = baseSpec + // Adds support for @Transient and @SerialName + // If you are not using them this is not required. + schemaConfigurator = KotlinXSchemaConfigurator() + } +} +``` diff --git a/docs/plugins/notarized_locations.md b/docs/plugins/notarized_locations.md new file mode 100644 index 0000000000..bc354cd865 --- /dev/null +++ b/docs/plugins/notarized_locations.md @@ -0,0 +1,62 @@ +The Ktor Locations API is an experimental API that allows users to add increased type safety to their defined routes. + +You can read more about it [here](https://ktor.io/docs/locations.html). + +Kompendium supports Locations through an ancillary module `kompendium-locations` + +## Adding the Artifact + +Prior to documenting your locations, you will need to add the artifact to your gradle build file. + +```kotlin +dependencies { + implementation("io.bkbn:kompendium-locations:latest.release") +} +``` + +## Installing Plugin + +Once you have installed the dependency, you can install the plugin. The `NotarizedLocations` plugin is an _application_ +level plugin, and **must** be install after both the `NotarizedApplication` plugin and the Ktor `Locations` plugin. + +```kotlin +private fun Application.mainModule() { + install(Locations) + install(NotarizedApplication()) { + spec = baseSpec + } + install(NotarizedLocations()) { + locations = mapOf( + Listing::class to NotarizedLocations.LocationMetadata( + parameters = listOf( + Parameter( + name = "name", + `in` = Parameter.Location.path, + schema = TypeDefinition.STRING + ), + Parameter( + name = "page", + `in` = Parameter.Location.path, + schema = TypeDefinition.INT + ) + ), + get = GetInfo.builder { + summary("Get user by id") + description("A very neat endpoint!") + response { + responseCode(HttpStatusCode.OK) + responseType() + description("Will return whether or not the user is real 😱") + } + } + ), + ) + } +} +``` + +Here, the `locations` property is a map of `KClass<*>` to metadata describing that locations metadata. This +metadata is functionally identical to how a standard `NotarizedRoute` is defined. + +> ⚠️ If you try to map a class that is not annotated with the ktor `@Location` annotation, you will get a runtime +> exception! diff --git a/docs/plugins/notarized_route.md b/docs/plugins/notarized_route.md new file mode 100644 index 0000000000..1dfa959670 --- /dev/null +++ b/docs/plugins/notarized_route.md @@ -0,0 +1,167 @@ +The `NotarizedRoute` plugin is the method by which you define the functionality and metadata for an individual route of +your API. + +# Route Level Metadata + +## Route Tags + +OpenAPI uses the concept of tags in order to logically group operations. Tags defined at the `NotarizedRoute` level will +be applied to _all_ operations defined in this route. In order to define tags for a specific operation, +see [below](#operation-tags). + +```kotlin +private fun Route.documentation() { + install(NotarizedRoute()) { + tags = setOf("User") + // will apply the User tag to all operations defined below + } +} +``` + +## Route Parameters + +Parameters can be defined at the route level. Doing so will assign the parameters to _all_ operations defined in this +route. In order to define parameters for a specific operation, see [below](#operation-parameters). + +```kotlin +private fun Route.documentation() { + install(NotarizedRoute()) { + parameters = listOf( + Parameter( + name = "id", + `in` = Parameter.Location.path, + schema = TypeDefinition.STRING + ) + ) + // Will apply the id parameter to all operations defined below + } +} +``` + +# Defining Operations + +Obviously, our route documentation would not be very useful without a way to detail the operations that are available at +the specified route. These operations will take the metadata defined, along with any existing info present at the route +level detailed [above](#route-level-metadata). Together, this defines an OpenAPI path operation. + +## Operation Builders + +Each HTTP Operation (Get, Put Post, Patch, Delete, Head, Options) has its own `Builder` that Kompendium uses to define +the necessary information to associate with the detailed operation. For example, a simple `GET` request could be +defined as follows. + +```kotlin +install(NotarizedRoute()) { + get = GetInfo.builder { + summary("Get ImportantDetail") + description("Retrieves an Important Detail from the database") + response { + responseCode(HttpStatusCode.OK) + responseType() + description("The Detail in Question") + } + } +} +``` + +## Operation Tags + +Operation tags work much like route tags, except they only apply to the operation they are defined in. They are defined +slightly differently, as a function on the builder, rather than an instance variable directly. + +```kotlin +install(NotarizedRoute()) { + get = GetInfo.builder { + tags("User") + // ... + } +} +``` + +## Operation Parameters + +Operation parameters work much like route parameters, except they only apply to the operation they are defined in. They +are defined slightly differently, as a function on the builder, rather than an instance variable directly. + +```kotlin +install(NotarizedRoute()) { + get = GetInfo.builder { + parameters( + Parameter( + name = "a", + `in` = Parameter.Location.path, + schema = TypeDefinition.STRING, + ), + Parameter( + name = "aa", + `in` = Parameter.Location.query, + schema = TypeDefinition.INT + ) + ) + // ... + } +} +``` + +## Response Info + +All operations are required to define a response info block, detailing the standard response that users of the API +should expect when performing this operation. At its most simple, doing so looks like the following + +```kotlin +get = GetInfo.builder { + summary("Get user by id") + description("A very neat endpoint!") + response { + responseCode(HttpStatusCode.OK) + responseType() + description("Will return whether or not the user is real 😱") + } +} +``` + +As you can see, we attach an http status code, a description, and finally the type that represents the payload that +users should expect. In order to indicate that no payload is expected, use `responseType()`. This should typically +be paired with a `204` status code. + +## Request Info + +On operations that allow a request body to be associated, you must also define a response info block so that Kompendium +can determine how to populate the required operation data. + +```kotlin +post = PostInfo.builder { + summary("Create User") + description("Will create a new user entity") + request { + requestType() + description("Data required to create new user") + } + response { + responseCode(HttpStatusCode.Created) + responseType() + description("User was created successfully") + } +} +``` + +## Error Info + +In addition to the standard response, you can attach additional responses via the `canRespond` function. + +```kotlin +get = GetInfo.builder { + summary("Get user by id") + description("A very neat endpoint!") + response { + responseCode(HttpStatusCode.OK) + responseType() + description("Will return whether or not the user is real 😱") + } + canRespond { + description("Bad Things Happened") + responseCode(HttpStatusCode.InternalServerError) + responseType() + } +} +```