Skip to content

Commit

Permalink
feat: Add opt-in locations support via ancillary module (#107)
Browse files Browse the repository at this point in the history
  • Loading branch information
brizzbuzz authored Nov 25, 2021
1 parent dd780ad commit 5e070e1
Show file tree
Hide file tree
Showing 22 changed files with 1,369 additions and 70 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## [1.11.0] - November 25th, 2021
### Added
- Support for Ktor Location Plugin

## [1.10.0] - November 25th, 2021

### Changed
Expand Down
167 changes: 104 additions & 63 deletions README.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Kompendium
project.version=1.10.0
project.version=1.11.0
# Kotlin
kotlin.code.style=official
# Gradle
Expand Down
3 changes: 2 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[versions]
kotlin = "1.4.32"
ktor = "1.6.4"
ktor = "1.6.5"
kotlinx-serialization = "1.2.1"
jackson-kotlin = "2.12.0"
slf4j = "1.7.30"
Expand All @@ -17,6 +17,7 @@ ktor-html-builder = { group = "io.ktor", name = "ktor-html-builder", version.ref
ktor-auth-lib = { group = "io.ktor", name = "ktor-auth", version.ref = "ktor" }
ktor-auth-jwt = { group = "io.ktor", name = "ktor-auth-jwt", version.ref = "ktor" }
ktor-webjars = { group = "io.ktor", name = "ktor-webjars", version.ref = "ktor" }
ktor-locations = { group = "io.ktor", name = "ktor-locations", version.ref = "ktor" }

# Serialization
jackson-module-kotlin = { group = "com.fasterxml.jackson.module", name = "jackson-module-kotlin", version.ref = "jackson-kotlin" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,9 @@ object MethodParser {
*/
private fun KType.toParameterSpec(): List<OpenApiSpecParameter> {
val clazz = classifier as KClass<*>
return clazz.memberProperties.map { prop ->
return clazz.memberProperties.filter { prop ->
prop.findAnnotation<KompendiumParam>() != null
}.map { prop ->
val field = prop.javaField?.type?.kotlin
?: error("Unable to parse field type from $prop")
val anny = prop.findAnnotation<KompendiumParam>()
Expand Down
78 changes: 78 additions & 0 deletions kompendium-locations/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
plugins {
`java-library`
`maven-publish`
signing
}

dependencies {
implementation(platform("org.jetbrains.kotlin:kotlin-bom"))
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
implementation(libs.bundles.ktor)
implementation(libs.ktor.locations)
implementation(projects.kompendiumCore)

testImplementation(libs.ktor.jackson)
testImplementation("org.jetbrains.kotlin:kotlin-test")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit")
testImplementation(libs.jackson.module.kotlin)
testImplementation(libs.ktor.server.test.host)
}

java {
withSourcesJar()
withJavadocJar()
}

publishing {
repositories {
maven {
name = "GithubPackages"
url = uri("https://maven.pkg.github.com/bkbnio/kompendium")
credentials {
username = System.getenv("GITHUB_ACTOR")
password = System.getenv("GITHUB_TOKEN")
}
}
}
publications {
create<MavenPublication>("kompendium") {
from(components["kotlin"])
artifact(tasks.sourcesJar)
artifact(tasks.javadocJar)
groupId = project.group.toString()
artifactId = project.name.toLowerCase()
version = project.version.toString()

pom {
name.set("Kompendium")
description.set("A minimally invasive OpenAPI spec generator for Ktor")
url.set("https://github.com/bkbnio/Kompendium")
licenses {
license {
name.set("MIT License")
url.set("https://mit-license.org/")
}
}
developers {
developer {
id.set("bkbnio")
name.set("Ryan Brink")
email.set("[email protected]")
}
}
scm {
connection.set("scm:git:git://github.com/bkbnio/Kompendium.git")
developerConnection.set("scm:git:ssh://github.com/bkbnio/Kompendium.git")
url.set("https://github.com/bkbnio/Kompendium.git")
}
}
}
}
}

signing {
val signingKey: String? by project
val signingPassword: String? by project
useInMemoryPgpKeys(signingKey, signingPassword)
sign(publishing.publications)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package io.bkbn.kompendium.locations

import io.bkbn.kompendium.Kompendium
import io.bkbn.kompendium.KompendiumPreFlight
import io.bkbn.kompendium.MethodParser.parseMethodInfo
import io.bkbn.kompendium.models.meta.MethodInfo.DeleteInfo
import io.bkbn.kompendium.models.meta.MethodInfo.GetInfo
import io.bkbn.kompendium.models.meta.MethodInfo.PostInfo
import io.bkbn.kompendium.models.meta.MethodInfo.PutInfo
import io.bkbn.kompendium.models.oas.OpenApiSpecPathItem
import io.ktor.application.ApplicationCall
import io.ktor.http.HttpMethod
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location
import io.ktor.locations.handle
import io.ktor.locations.location
import io.ktor.routing.Route
import io.ktor.routing.method
import io.ktor.util.pipeline.PipelineContext
import kotlin.reflect.KClass
import kotlin.reflect.full.findAnnotation

/**
* This version of notarized routes leverages the Ktor [io.ktor.locations.Locations] plugin to provide type safe access
* to all path and query parameters.
*/
@KtorExperimentalLocationsAPI
object NotarizedLocation {

/**
* Notarization for an HTTP GET request leveraging the Ktor [io.ktor.locations.Locations] plugin
* @param TParam The class containing all parameter fields.
* Each field must be annotated with @[io.bkbn.kompendium.annotations.KompendiumField].
* Additionally, the class must be annotated with @[io.ktor.locations.Location].
* @param TResp Class detailing the expected API response
* @param info Route metadata
*/
inline fun <reified TParam : Any, reified TResp : Any> Route.notarizedGet(
info: GetInfo<TParam, TResp>,
noinline body: suspend PipelineContext<Unit, ApplicationCall>.(TParam) -> Unit
): Route =
KompendiumPreFlight.methodNotarizationPreFlight<TParam, Unit, TResp>() { paramType, requestType, responseType ->
val locationAnnotation = TParam::class.findAnnotation<Location>()
require(locationAnnotation != null) { "Location annotation must be present to leverage notarized location api" }
val path = Kompendium.calculatePath(this)
val locationPath = TParam::class.calculatePath()
val pathWithLocation = path.plus(locationPath)
Kompendium.openApiSpec.paths.getOrPut(pathWithLocation) { OpenApiSpecPathItem() }
Kompendium.openApiSpec.paths[pathWithLocation]?.get = parseMethodInfo(info, paramType, requestType, responseType)
return location(TParam::class) {
method(HttpMethod.Get) { handle(body) }
}
}

/**
* Notarization for an HTTP POST request leveraging the Ktor [io.ktor.locations.Locations] plugin
* @param TParam The class containing all parameter fields.
* Each field must be annotated with @[io.bkbn.kompendium.annotations.KompendiumField]
* Additionally, the class must be annotated with @[io.ktor.locations.Location].
* @param TReq Class detailing the expected API request body
* @param TResp Class detailing the expected API response
* @param info Route metadata
*/
inline fun <reified TParam : Any, reified TReq : Any, reified TResp : Any> Route.notarizedPost(
info: PostInfo<TParam, TReq, TResp>,
noinline body: suspend PipelineContext<Unit, ApplicationCall>.(TParam) -> Unit
): Route =
KompendiumPreFlight.methodNotarizationPreFlight<TParam, TReq, TResp>() { paramType, requestType, responseType ->
val locationAnnotation = TParam::class.findAnnotation<Location>()
require(locationAnnotation != null) { "Location annotation must be present to leverage notarized location api" }
val path = Kompendium.calculatePath(this)
val locationPath = TParam::class.calculatePath()
val pathWithLocation = path.plus(locationPath)
Kompendium.openApiSpec.paths.getOrPut(pathWithLocation) { OpenApiSpecPathItem() }
Kompendium.openApiSpec.paths[pathWithLocation]?.post = parseMethodInfo(info, paramType, requestType, responseType)
return location(TParam::class) {
method(HttpMethod.Post) { handle(body) }
}
}

/**
* Notarization for an HTTP Delete request leveraging the Ktor [io.ktor.locations.Locations] plugin
* @param TParam The class containing all parameter fields.
* Each field must be annotated with @[io.bkbn.kompendium.annotations.KompendiumField]
* Additionally, the class must be annotated with @[io.ktor.locations.Location].
* @param TReq Class detailing the expected API request body
* @param TResp Class detailing the expected API response
* @param info Route metadata
*/
inline fun <reified TParam : Any, reified TReq : Any, reified TResp : Any> Route.notarizedPut(
info: PutInfo<TParam, TReq, TResp>,
noinline body: suspend PipelineContext<Unit, ApplicationCall>.(TParam) -> Unit
): Route =
KompendiumPreFlight.methodNotarizationPreFlight<TParam, TReq, TResp>() { paramType, requestType, responseType ->
val locationAnnotation = TParam::class.findAnnotation<Location>()
require(locationAnnotation != null) { "Location annotation must be present to leverage notarized location api" }
val path = Kompendium.calculatePath(this)
val locationPath = TParam::class.calculatePath()
val pathWithLocation = path.plus(locationPath)
Kompendium.openApiSpec.paths.getOrPut(pathWithLocation) { OpenApiSpecPathItem() }
Kompendium.openApiSpec.paths[pathWithLocation]?.put =
parseMethodInfo(info, paramType, requestType, responseType)
return location(TParam::class) {
method(HttpMethod.Put) { handle(body) }
}
}

/**
* Notarization for an HTTP POST request leveraging the Ktor [io.ktor.locations.Locations] plugin
* @param TParam The class containing all parameter fields.
* Each field must be annotated with @[io.bkbn.kompendium.annotations.KompendiumField]
* Additionally, the class must be annotated with @[io.ktor.locations.Location].
* @param TResp Class detailing the expected API response
* @param info Route metadata
*/
inline fun <reified TParam : Any, reified TResp : Any> Route.notarizedDelete(
info: DeleteInfo<TParam, TResp>,
noinline body: suspend PipelineContext<Unit, ApplicationCall>.(TParam) -> Unit
): Route =
KompendiumPreFlight.methodNotarizationPreFlight<TParam, Unit, TResp> { paramType, requestType, responseType ->
val locationAnnotation = TParam::class.findAnnotation<Location>()
require(locationAnnotation != null) { "Location annotation must be present to leverage notarized location api" }
val path = Kompendium.calculatePath(this)
val locationPath = TParam::class.calculatePath()
val pathWithLocation = path.plus(locationPath)
Kompendium.openApiSpec.paths.getOrPut(pathWithLocation) { OpenApiSpecPathItem() }
Kompendium.openApiSpec.paths[pathWithLocation]?.delete =
parseMethodInfo(info, paramType, requestType, responseType)
return location(TParam::class) {
method(HttpMethod.Delete) { handle(body) }
}
}

fun KClass<*>.calculatePath(suffix: String = ""): String {
val locationAnnotation = this.findAnnotation<Location>()
require(locationAnnotation != null) { "Location annotation must be present to leverage notarized location api" }
val parent = this.java.declaringClass?.kotlin
val newSuffix = locationAnnotation.path.plus(suffix)
return when (parent) {
null -> newSuffix
else -> parent.calculatePath(newSuffix)
}
}
}
Loading

0 comments on commit 5e070e1

Please sign in to comment.