-
Notifications
You must be signed in to change notification settings - Fork 27
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Add opt-in locations support via ancillary module (#107)
- Loading branch information
Showing
22 changed files
with
1,369 additions
and
70 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
144 changes: 144 additions & 0 deletions
144
kompendium-locations/src/main/kotlin/io/bkbn/kompendium/locations/NotarizedLocation.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} | ||
} |
Oops, something went wrong.