diff --git a/.tool-versions b/.tool-versions
index a54bc706e..b76921ca0 100644
--- a/.tool-versions
+++ b/.tool-versions
@@ -1,2 +1,3 @@
kotlin 1.5.0-M2
java openjdk-14.0.1
+gradle 7.0
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 16bbfd887..554f58abc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,10 @@
+## [0.0.2] - April 12th, 2021
+
+### Added
+
+- Beginning of an implementation. Currently, able to generate a rough outline of the API at runtime, along with generating
+full data classes represented by JSON Schema.
+
## [0.0.1] - April 11th, 2021
### Added
diff --git a/README.md b/README.md
index abf8d2e0d..f68ca2027 100644
--- a/README.md
+++ b/README.md
@@ -2,10 +2,56 @@
## What is Kompendium
-Kompendium is intended to be a non-intrusive OpenApi Specification generator for [Ktor](https://ktor.io).
-Non-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](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.
## Modules
TODO
+
+## Examples
+
+```kotlin
+// Minimal API Example
+fun Application.mainModule() {
+ install(ContentNegotiation) {
+ jackson()
+ }
+ routing {
+ route("/test") {
+ route("/{id}") {
+ notarizedGet(testIdGetInfo) {
+ call.respondText("get by id")
+ }
+ }
+ route("/single") {
+ notarizedGet(testSingleGetInfo) {
+ call.respondText("get single")
+ }
+ notarizedPost(testSinglePostInfo) {
+ call.respondText("test post")
+ }
+ notarizedPut(testSinglePutInfo) {
+ call.respondText { "hey" }
+ }
+ }
+ }
+ route("/openapi.json") {
+ get {
+ call.respond(openApiSpec.copy(
+ info = OpenApiSpecInfo(
+ title = "Test API",
+ version = "1.3.3.7",
+ description = "An amazing, fully-ish 😉 generated API spec"
+ )
+ ))
+ }
+ }
+ }
+}
+```
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 352ddde00..f371643ee 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions-snapshots/gradle-7.1-20210328220041+0000-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/kompendium-core/build.gradle.kts b/kompendium-core/build.gradle.kts
index 665d3ea55..50c08531f 100644
--- a/kompendium-core/build.gradle.kts
+++ b/kompendium-core/build.gradle.kts
@@ -6,6 +6,7 @@ plugins {
dependencies {
implementation(platform("org.jetbrains.kotlin:kotlin-bom"))
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
+ implementation(libs.bundles.ktor)
testImplementation("org.jetbrains.kotlin:kotlin-test")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit")
testImplementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.12.0")
diff --git a/kompendium-core/src/main/kotlin/org/leafygreens/kompendium/Kompendium.kt b/kompendium-core/src/main/kotlin/org/leafygreens/kompendium/Kompendium.kt
index a5459bd6a..43e813eb3 100644
--- a/kompendium-core/src/main/kotlin/org/leafygreens/kompendium/Kompendium.kt
+++ b/kompendium-core/src/main/kotlin/org/leafygreens/kompendium/Kompendium.kt
@@ -1,12 +1,128 @@
package org.leafygreens.kompendium
-import org.leafygreens.kompendium.models.OpenApiSpec
-import org.leafygreens.kompendium.models.OpenApiSpecInfo
+import io.ktor.application.ApplicationCall
+import io.ktor.http.HttpMethod
+import io.ktor.routing.Route
+import io.ktor.routing.createRouteFromPath
+import io.ktor.routing.method
+import io.ktor.util.pipeline.PipelineInterceptor
+import java.lang.reflect.ParameterizedType
+import kotlin.reflect.KClass
+import kotlin.reflect.KProperty
+import kotlin.reflect.full.findAnnotation
+import kotlin.reflect.full.memberProperties
+import kotlin.reflect.jvm.javaField
+import org.leafygreens.kompendium.annotations.KompendiumField
+import org.leafygreens.kompendium.annotations.KompendiumInternal
+import org.leafygreens.kompendium.models.oas.ArraySchema
+import org.leafygreens.kompendium.models.oas.FormatSchema
+import org.leafygreens.kompendium.models.oas.ObjectSchema
+import org.leafygreens.kompendium.models.oas.OpenApiSpec
+import org.leafygreens.kompendium.models.oas.OpenApiSpecComponentSchema
+import org.leafygreens.kompendium.models.oas.OpenApiSpecInfo
+import org.leafygreens.kompendium.models.oas.OpenApiSpecPathItem
+import org.leafygreens.kompendium.models.oas.OpenApiSpecPathItemOperation
+import org.leafygreens.kompendium.models.oas.SimpleSchema
+import org.leafygreens.kompendium.models.meta.MethodInfo
+import org.leafygreens.kompendium.util.Helpers.calculatePath
+import org.leafygreens.kompendium.util.Helpers.putPairIfAbsent
-class Kompendium {
- val spec = OpenApiSpec(
+object Kompendium {
+ val openApiSpec = OpenApiSpec(
info = OpenApiSpecInfo(),
servers = mutableListOf(),
paths = mutableMapOf()
)
+
+ fun Route.notarizedGet(info: MethodInfo, body: PipelineInterceptor): Route {
+ val path = calculatePath()
+ openApiSpec.paths.getOrPut(path) { OpenApiSpecPathItem() }
+ openApiSpec.paths[path]?.get = OpenApiSpecPathItemOperation(
+ summary = info.summary,
+ description = info.description,
+ tags = info.tags
+ )
+ return method(HttpMethod.Get) { handle(body) }
+ }
+
+ inline fun Route.notarizedPost(
+ info: MethodInfo,
+ noinline body: PipelineInterceptor
+ ): Route = generateComponentSchemas(info, body) { i, b ->
+ val path = calculatePath()
+ openApiSpec.paths.getOrPut(path) { OpenApiSpecPathItem() }
+ openApiSpec.paths[path]?.post = OpenApiSpecPathItemOperation(
+ summary = i.summary,
+ description = i.description,
+ tags = i.tags
+ )
+ return method(HttpMethod.Post) { handle(b) }
+ }
+
+ inline fun Route.notarizedPut(
+ info: MethodInfo,
+ noinline body: PipelineInterceptor,
+ ): Route = generateComponentSchemas(info, body) { i, b ->
+ val path = calculatePath()
+ openApiSpec.paths.getOrPut(path) { OpenApiSpecPathItem() }
+ openApiSpec.paths[path]?.put = OpenApiSpecPathItemOperation(
+ summary = i.summary,
+ description = i.description,
+ tags = i.tags
+ )
+ return method(HttpMethod.Put) { handle(b) }
+ }
+
+ @OptIn(KompendiumInternal::class)
+ inline fun generateComponentSchemas(
+ info: MethodInfo,
+ noinline body: PipelineInterceptor,
+ block: (MethodInfo, PipelineInterceptor) -> Route
+ ): Route {
+ openApiSpec.components.schemas.putPairIfAbsent(objectSchemaPair(TQ::class))
+ openApiSpec.components.schemas.putPairIfAbsent(objectSchemaPair(TR::class))
+ openApiSpec.components.schemas.putPairIfAbsent(objectSchemaPair(TP::class))
+ return block.invoke(info, body)
+ }
+
+ @KompendiumInternal
+ // TODO Investigate a caching mechanism to reduce overhead... then just reference once created
+ fun objectSchemaPair(clazz: KClass<*>): Pair {
+ val o = objectSchema(clazz)
+ return Pair(clazz.qualifiedName!!, o)
+ }
+
+ private fun objectSchema(clazz: KClass<*>): ObjectSchema =
+ ObjectSchema(properties = clazz.memberProperties.associate { prop ->
+ val field = prop.javaField?.type?.kotlin
+ val anny = prop.findAnnotation()
+ val schema = when (field) {
+ List::class -> listFieldSchema(prop)
+ else -> fieldToSchema(field as KClass<*>)
+ }
+
+ val name = anny?.let {
+ anny.name
+ } ?: prop.name
+
+ Pair(name, schema)
+ })
+
+ private fun listFieldSchema(prop: KProperty<*>): ArraySchema {
+ val listType = ((prop.javaField?.genericType
+ as ParameterizedType).actualTypeArguments.first()
+ as Class<*>).kotlin
+ return ArraySchema(fieldToSchema(listType))
+ }
+
+ @OptIn(KompendiumInternal::class)
+ private fun fieldToSchema(field: KClass<*>): OpenApiSpecComponentSchema = when (field) {
+ Int::class -> FormatSchema("int32", "integer")
+ Long::class -> FormatSchema("int64", "integer")
+ Double::class -> FormatSchema("double", "number")
+ Float::class -> FormatSchema("float", "number")
+ String::class -> SimpleSchema("string")
+ Boolean::class -> SimpleSchema("boolean")
+ else -> objectSchema(field)
+ }
}
diff --git a/kompendium-core/src/main/kotlin/org/leafygreens/kompendium/annotations/KompendiumField.kt b/kompendium-core/src/main/kotlin/org/leafygreens/kompendium/annotations/KompendiumField.kt
new file mode 100644
index 000000000..cc64f8da0
--- /dev/null
+++ b/kompendium-core/src/main/kotlin/org/leafygreens/kompendium/annotations/KompendiumField.kt
@@ -0,0 +1,5 @@
+package org.leafygreens.kompendium.annotations
+
+@Retention(AnnotationRetention.RUNTIME)
+@Target(AnnotationTarget.PROPERTY)
+annotation class KompendiumField(val name: String)
diff --git a/kompendium-core/src/main/kotlin/org/leafygreens/kompendium/annotations/KompendiumInternal.kt b/kompendium-core/src/main/kotlin/org/leafygreens/kompendium/annotations/KompendiumInternal.kt
new file mode 100644
index 000000000..2146e2350
--- /dev/null
+++ b/kompendium-core/src/main/kotlin/org/leafygreens/kompendium/annotations/KompendiumInternal.kt
@@ -0,0 +1,18 @@
+package org.leafygreens.kompendium.annotations
+
+@Suppress("DEPRECATION")
+@RequiresOptIn(
+ level = RequiresOptIn.Level.WARNING,
+ message = "This API internal to Kompendium and should not be used. It could be removed or changed without notice."
+)
+@Experimental(level = Experimental.Level.WARNING)
+@Target(
+ AnnotationTarget.CLASS,
+ AnnotationTarget.TYPEALIAS,
+ AnnotationTarget.FUNCTION,
+ AnnotationTarget.PROPERTY,
+ AnnotationTarget.FIELD,
+ AnnotationTarget.CONSTRUCTOR,
+ AnnotationTarget.PROPERTY_SETTER
+)
+annotation class KompendiumInternal
diff --git a/kompendium-core/src/main/kotlin/org/leafygreens/kompendium/models/OpenApiSpec.kt b/kompendium-core/src/main/kotlin/org/leafygreens/kompendium/models/OpenApiSpec.kt
deleted file mode 100644
index 7872cea12..000000000
--- a/kompendium-core/src/main/kotlin/org/leafygreens/kompendium/models/OpenApiSpec.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-package org.leafygreens.kompendium.models
-
-data class OpenApiSpec(
- val openapi: String = "3.0.3",
- val info: OpenApiSpecInfo? = null,
- // TODO Needs to default to server object with url of `/`
- val servers: MutableList? = null,
- val paths: MutableMap? = null,
- val components: OpenApiSpecComponents? = null,
- // todo needs to reference objects in the components -> security scheme 🤔
- val security: List