feat: v2-alpha (#112)

There are still some bugs, still some outstanding features, but I don't want to hold this back any longer, that way I can keep the future PRs much more focused
This commit is contained in:
Ryan Brink
2022-01-02 23:15:15 -05:00
committed by GitHub
parent d66880f9b2
commit c29567114d
229 changed files with 9172 additions and 7233 deletions

374
README.md
View File

@ -1,20 +1,24 @@
# Kompendium
[![Codacy Badge](https://app.codacy.com/project/badge/Grade/a9bfd6c77d22497b907b3221849a3ba9)](https://www.codacy.com/gh/bkbnio/kompendium/dashboard?utm_source=github.com&utm_medium=referral&utm_content=bkbnio/kompendium&utm_campaign=Badge_Grade)
[![Codacy Badge](https://app.codacy.com/project/badge/Coverage/a9bfd6c77d22497b907b3221849a3ba9)](https://www.codacy.com/gh/bkbnio/kompendium/dashboard?utm_source=github.com&utm_medium=referral&utm_content=bkbnio/kompendium&utm_campaign=Badge_Coverage)
[![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)
- [Library Details](#library-details)
- [Local Development](#local-development)
- [The Playground](#the-playground)
## What is Kompendium
### ⚠️ For info on V2 please see [here](#V2)
Kompendium is intended to be a minimally invasive OpenApi Specification generator for [Ktor](https://ktor.io). 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.
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.
## How to install
Kompendium publishes all releases to Maven Central. As such, using the stable version of `Kompendium` is as simple as
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
@ -23,42 +27,19 @@ repositories {
}
dependencies {
implementation("io.bkbn:kompendium-core:1.8.1")
implementation("io.bkbn:kompendium-auth:1.8.1")
implementation("io.bkbn:kompendium-swagger-ui:1.8.1")
// Other dependencies...
implementation("io.bkbn:kompendium-core:latest.release")
}
```
The last two dependencies are optional.
If you want to get a little spicy 🤠 every merge of Kompendium is published to the GitHub package registry. Pulling from
GitHub is slightly more involved, but such is the price you pay for bleeding edge fake data generation.
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)
```kotlin
// 1 Setup a helper function to import any Github Repository Package
// This step is optional but I have a bunch of stuff stored on github so I find it useful 😄
fun RepositoryHandler.github(packageUrl: String) = maven {
name = "GithubPackages"
url = uri(packageUrl)
credentials {
username = java.lang.System.getenv("GITHUB_USER")
password = java.lang.System.getenv("GITHUB_TOKEN")
}
}
# Library Details
// 2 Add the repo in question (in this case Kompendium)
repositories {
github("https://maven.pkg.github.com/bkbnio/kompendium")
}
// 3 Add the package like any normal dependency
dependencies {
implementation("io.bkbn:kompendium-core:1.8.1")
}
```
Details on how to use Kompendium can be found in the Wiki (WIP, this is new for V2 and will be fleshed out prior to release)
## Local Development
@ -66,320 +47,9 @@ Kompendium should run locally right out of the box, no configuration necessary (
New features can be built locally and published to your local maven repository with the `./gradlew publishToMavenLocal`
command!
## In depth
## The Playground
### Notarized Routes
This repo contains a `playground` module that contains a number of working examples showcasing the capabilities of
Kompendium.
Kompendium introduces the concept of `notarized` HTTP methods. That is, for all your `GET`, `POST`, `PUT`, and `DELETE`
operations, there is a corresponding `notarized` method. These operations are strongly typed, and use reification for a
lot of the class based reflection that powers Kompendium. Generally speaking the three types that a `notarized` method
will consume are
- `TParam`: Used to notarize expected request parameters
- `TReq`: Used to build the object schema for a request body
- `TResp`: Used to build the object schema for a response body
`GET` and `DELETE` take `TParam` and `TResp` while `PUT` and `POST` take all three.
In addition to standard HTTP Methods, Kompendium also introduced the concept of `notarizedExceptions`. Using
the `StatusPage`
extension, users can notarize all handled exceptions, along with their respective HTTP codes and response types.
Exceptions that have been `notarized` require two types as supplemental information
- `TErr`: Used to notarize the exception being handled by this use case. Used for matching responses at the route level.
- `TResp`: Same as above, this dictates the expected return type of the error response.
In keeping with minimal invasion, these extension methods all consume the same code block as a standard Ktor route
method, meaning that swapping in a default Ktor route and a Kompendium `notarized` route is as simple as a single method
change.
### Supplemental Annotations
In general, Kompendium tries to limit the number of annotations that developers need to use in order to get an app
integrated.
Currently, the annotations used by Kompendium are as follows
- `KompendiumField`
- `KompendiumParam`
The intended purpose of `KompendiumField` is to offer field level overrides such as naming conventions (ie snake instead
of camel).
The purpose of `KompendiumParam` is to provide supplemental information needed to properly assign the type of parameter
(cookie, header, query, path) as well as other parameter-level metadata.
Using backticks, users can use data classes to represent things like header names as follows
```kotlin
data class HeaderNameTest(
@KompendiumParam(type = ParamType.HEADER) val `X-UserEmail`: String
)
```
### Undeclared Field
There is also a final `UndeclaredField` annotation. This should be used only in an absolutely emergency. This annotation
will allow you to inject a _single_ undeclared field that will be included as part of the schema.
Due to limitations in using repeated annotations, this can only be used once per class
This is a complete hack, and is included for odd scenarios like kotlinx serialization polymorphic adapters that expect a
`type` field in order to perform their analysis.
Use this _only_ when **all** else fails
### Polymorphism
Speaking of polymorphism... out of the box, Kompendium has support for sealed classes and interfaces. At runtime, it
will build a mapping of all available sub-classes and build a spec that takes `anyOf` the implementations. This is
currently a weak point of the entire library, and suggestions on better implementations are welcome 🤠
### Serialization
Under the hood, Kompendium uses Jackson to serialize the final api spec. However, this implementation detail does not
leak to the actual API, meaning that users are free to choose the serialization library of their choice.
Added the possibility to add your own ObjectMapper for Jackson.
Added a default parameter with the following configuration:
```kotlin
ObjectMapper()
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
.enable(SerializationFeature.INDENT_OUTPUT)
```
If you want to change this default configuration and use your own ObjectMapper you only need to pass it as a second
argument to the openApi module:
```kotlin
routing {
openApi(oas, objectMapper)
route("/potato/spud") {
notarizedGet(simpleGetInfo) {
call.respond(HttpStatusCode.OK)
}
}
}
```
### Route Handling
> ⚠️ Warning: Custom route handling is almost definitely an indication that either a new selector should be added to kompendium-core or that kompendium is in need of another module to handle a new ktor companion module. If you have encountered a route selector that is not already handled, please consider opening an [issue](https://github.com/bkbnio/kompendium/issues/new)
Kompendium does its best to handle all Ktor routes out of the gate. However, in keeping with the modular approach of
Ktor and Kompendium, this is not always possible.
Should you need to, custom route handlers can be registered via the
`Kompendium.addCustomRouteHandler` function.
The handler signature is as follows
```kotlin
fun <T : RouteSelector> addCustomRouteHandler(
selector: KClass<T>,
handler: PathCalculator.(Route, String) -> String
)
```
This function takes a selector, which _must_ be a KClass of the Ktor `RouteSelector` type. The handler is a function
that extends the Kompendium `PathCalculator`. This is necessary because it gives you access
to `PathCalculator.calculate`, which you are going to want in order to recursively calculate the remainder of the
route :)
Its parameters are the `Route` itself, along with the "tail" of the Path (the path that has been calculated thus far).
Working examples `init` blocks of the `PathCalculator` and `KompendiumAuth` object.
### Ktor Locations Plugin
Kompendium offers integration with the Ktor [Locations](https://ktor.io/docs/locations.html) plugin via an optional
module `kompendium-locations`.
This provides a wrapper around the Locations api, provide full path analysis, along with typesafe access to request
parameters. However, due to the way that Kompendium core parses class metadata, you will still need to annotate all
parameters with `@KompendiumParam`. A simple example
```kotlin
@Location("/test/{name}")
data class TestLocations(
@KompendiumParam(ParamType.PATH)
val name: String,
)
```
When passed to a notarized route (make sure you are using the notarized methods found in
the `io.bkbn.kompendium.locations` package ⚠️), this will provide you with a notarized route that also passes through
the type safe instance containing all of your parameters!
```kotlin
notarizedGet(testLocation) { tl ->
call.respondText { tl.name }
}
```
A working example can be found in the `kompendium-playground` in the `LocationsSpike` file :)
## Examples
The full source code can be found in the `kompendium-playground` module. Here is a simple get endpoint example
```kotlin
// Minimal API Example
fun Application.mainModule() {
install(StatusPages) {
notarizedException<Exception, ExceptionResponse>(
info = ResponseInfo(
KompendiumHttpCodes.BAD_REQUEST,
"Bad Things Happened"
)
) {
call.respond(HttpStatusCode.BadRequest, ExceptionResponse("Why you do dis?"))
}
}
routing {
openApi(oas)
redoc(oas)
swaggerUI()
route("/potato/spud") {
notarizedGet(simpleGetInfo) {
call.respond(HttpStatusCode.OK)
}
}
}
}
val simpleGetInfo = GetInfo<Unit, ExampleResponse>(
summary = "Example Parameters",
description = "A test for setting parameter examples",
operationId = "getExamples",
responseInfo = ResponseInfo(
status = 200,
description = "nice",
examples = mapOf("test" to ExampleResponse(c = "spud"))
),
canThrow = setOf(Exception::class)
)
```
### Kompendium Auth and security schemes
There is a separate library to handle security schemes: `kompendium-auth`. This needs to be added to your project as
dependency.
At the moment, the basic and jwt authentication is only supported.
A minimal example would be:
```kotlin
install(Authentication) {
notarizedBasic("basic") {
realm = "Ktor realm 1"
// configure basic authentication provider..
}
notarizedJwt("jwt") {
realm = "Ktor realm 2"
// configure jwt authentication provider...
}
}
routing {
authenticate("basic") {
route("/basic_auth") {
notarizedGet(basicAuthGetInfo) {
call.respondText { "basic auth" }
}
}
}
authenticate("jwt") {
route("/jwt") {
notarizedGet(jwtAuthGetInfo) {
call.respondText { "jwt" }
}
}
}
}
val basicAuthGetInfo = MethodInfo<Unit, ExampleResponse>(
summary = "Another get test",
description = "testing more",
responseInfo = testGetResponse,
securitySchemes = setOf("basic")
)
val jwtAuthGetInfo = basicAuthGetInfo.copy(securitySchemes = setOf("jwt"))
```
### Enabling Swagger ui
To enable Swagger UI, `kompendium-swagger-ui` needs to be added. This will also add
the [ktor webjars feature](https://ktor.io/docs/webjars.html) to your classpath as it is required for swagger ui.
Minimal Example:
```kotlin
install(Webjars)
routing {
openApi(oas)
swaggerUI()
}
```
### Enabling ReDoc
Unlike swagger, redoc is provided (perhaps confusingly, in the `core` module). This means out of the box
with `kompendium-core`, you can add
[ReDoc](https://github.com/Redocly/redoc) as follows
```kotlin
routing {
openApi(oas)
redoc(oas)
}
```
## Custom Type Overrides
Kompendium does its best to analyze types and to generate an OpenAPI format accordingly. However, there are certain
classes that just don't play nice with the standard reflection analysis that Kompendium performs. Should you encounter a
data type that Kompendium cannot comprehend, you will need to add it explicitly. For example, adding the Joda
Time `DateTime` object would be as simple as the following
```kotlin
Kompendium.addCustomTypeSchema(DateTime::class, FormatSchema("date-time", "string"))
```
Since `Kompendium` is an object, this needs to be declared once, ahead of the actual API instantiation. This way, this
type override can be cached ahead of reflection. Kompendium will then match all instances of this type and return the
specified schema.
So how do you know a type can and cannot be inferred? The safe bet is that it can be. So go ahead and give it a shot.
However, in the very odd scenario (almost always having to do with date/time libraries 😤) where it can't, you can rest
safely knowing that you have the option to inject a custom override should you need to.
## Limitations
### Kompendium as a singleton
Currently, Kompendium exists as a Kotlin object. This comes with a couple perks, but a couple downsides. Primarily, it
offers a seriously clean UX where the implementer doesn't need to worry about what instance to send data to. The main
drawback, however, is that you are limited to a single API per classpath.
If this is a blocker, please open a GitHub issue, and we can start to think out solutions!
## Future Work
Work on V1 of Kompendium has come to a close. This, however, does not mean it has achieved complete parity with the
OpenAPI feature spec, nor does it have all-of-the nice to have features that a truly next-gen API spec should have.
There are several outstanding features that have been added to the
[V2 Milestone](https://github.com/bkbnio/kompendium/milestone/2). Among others, this includes
- AsyncAPI Integration
- Field Validation
If you have a feature that you would like to see implemented that is not on this list, or discover a 🐞, please open an
issue [here](https://github.com/bkbnio/kompendium/issues/new)
### V2
Due to the large number of breaking changes that will be made in version 2, development is currently being done on the
long-lived `v2` feature branch. If you are working on any feature in the `V2` milestone, please target that branch!
If you are unsure where your changes should be, please open an issue first :)
Feel free to check it out, or even create your own example!