Skip to content

Commit

Permalink
Merge pull request #9 from nomisRev/api-generation
Browse files Browse the repository at this point in the history
API generation
  • Loading branch information
nomisRev authored Jun 11, 2024
2 parents 7a4839b + 1666df6 commit f056d42
Show file tree
Hide file tree
Showing 77 changed files with 3,024 additions and 2,656 deletions.
4 changes: 2 additions & 2 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@ ij_kotlin_method_call_chain_wrap = normal
ij_kotlin_method_parameters_new_line_after_left_paren = true
ij_kotlin_method_parameters_right_paren_on_new_line = true
ij_kotlin_method_parameters_wrap = on_every_item
ij_kotlin_name_count_to_use_star_import = 5
ij_kotlin_name_count_to_use_star_import_for_members = 3
ij_kotlin_name_count_to_use_star_import = 99999
ij_kotlin_name_count_to_use_star_import_for_members = 99999
ij_kotlin_packages_to_use_import_on_demand = java.util.*,kotlinx.android.synthetic.**,io.ktor.**
ij_kotlin_parameter_annotation_wrap = off
ij_kotlin_space_after_comma = true
Expand Down
7 changes: 5 additions & 2 deletions README.MD
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
# OpenKTTP
# OpenAPI-kt

**WORK IN PROGRESS**

OpenKTTP is a toolset for working with OpenAPI in Kotlin.
This project exists out of several pieces, and they can be combined in different ways to achieve different goals.

- Core: A OpenAPI parser, and typed ADT based on KotlinX Serialization
- OpenAPI Typed: A version of the `Core` ADT, structures the data in a convenient way to retrieve.
- Generic: A `Generic` ADT that allows working with content regardless of its format.
- Generator: A code generator that generates code from the `OpenAPI Typed` ADT
- Gradle Plugin: Gradle plugin to conveniently generate clients
55 changes: 55 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,4 +1,59 @@
import com.diffplug.gradle.spotless.SpotlessExtension
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
import org.gradle.jvm.tasks.Jar
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.jetbrains.kotlin.powerassert.gradle.PowerAssertGradleExtension

plugins {
alias(libs.plugins.multiplatform) apply false
alias(libs.plugins.assert)
alias(libs.plugins.publish)
alias(libs.plugins.spotless)
}

configure<SpotlessExtension> {
kotlin {
target("**/*.kt")
ktfmt().kotlinlangStyle().configure {
it.setBlockIndent(2)
it.setContinuationIndent(2)
it.setRemoveUnusedImport(true)
}
trimTrailingWhitespace()
endWithNewline()
}
}

@Suppress("OPT_IN_USAGE")
configure<PowerAssertGradleExtension> {
functions = listOf(
"kotlin.test.assertEquals",
"kotlin.test.assertTrue"
)
}

subprojects {
tasks {
withType(Jar::class.java) {
manifest {
attributes("Automatic-Module-Name" to "io.github.nomisrev")
}
}
withType<JavaCompile> {
options.release.set(8)
}
withType<KotlinCompile> {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_1_8)
}
}
withType<Test> {
useJUnitPlatform()
testLogging {
exceptionFormat = TestExceptionFormat.FULL
events("SKIPPED", "FAILED")
}
}
}
}
3 changes: 1 addition & 2 deletions example/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import io.github.nomisrev.openapi.generation.NamingStrategy

plugins {
kotlin("multiplatform") version "2.0.0"
id("io.github.nomisrev.openapi.plugin") version "1.0.0"
Expand All @@ -12,6 +10,7 @@ kotlin {
commonMain {
kotlin.srcDir(project.file("build/generated/openapi/src/commonMain/kotlin"))
dependencies {
implementation("io.ktor:ktor-client-core:2.3.6")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
}
}
Expand Down
5 changes: 5 additions & 0 deletions example/src/commonMain/kotlin/main.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package io.github.nomisrev.openapi

val x: OpenAPI = TODO()

suspend fun main() {}
24 changes: 8 additions & 16 deletions generation/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,13 @@ plugins {
id(libs.plugins.publish.get().pluginId)
}

@Suppress("OPT_IN_USAGE")
powerAssert {
functions = listOf("kotlin.test.assertEquals", "kotlin.test.assertTrue")
}

kotlin {
// explicitApi()
jvm {
@OptIn(ExperimentalKotlinGradlePluginApi::class)
mainRun { mainClass.set("io.github.nomisrev.openapi.MainKt") }
}
macosArm64 {
binaries {
executable { entryPoint = "main" }
}
}
linuxX64()
// macosArm64()
// linuxX64()

sourceSets {
commonMain {
Expand All @@ -32,6 +22,7 @@ kotlin {
dependencies {
api(libs.kasechange)
api(libs.okio)
implementation("io.ktor:ktor-client-core:2.3.6")
api(projects.parser)
}
}
Expand All @@ -40,6 +31,11 @@ kotlin {
implementation(libs.test)
}
}
jvmMain {
dependencies {
implementation("com.squareup:kotlinpoet:1.17.0")
}
}
// jsMain {
// dependencies {
// implementation("com.squareup.okio:okio-nodefilesystem:3.9.0")
Expand All @@ -53,10 +49,6 @@ kotlin {
}
}

tasks.withType<Test> {
useJUnitPlatform()
}

task("runMacosArm64") {
dependsOn("linkDebugExecutableMacosArm64")
dependsOn("runDebugExecutableMacosArm64")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package io.github.nomisrev.openapi

// TODO: make hard-coded.
// We're opinionated about the API structure
internal fun interface ApiSorter {
fun sort(routes: Iterable<Route>): Root

companion object {
val ByPath: ApiSorter = ByPathApiSorter
}
}

/**
* ADT that models how to generate the API. Our OpenAPI document dictates the structure of the API,
* so all operations are available as their path, with operationId. i.e. for `OpenAI`
* `/chat/completions` with operationId `createChatCompletion`.
*
* interface OpenAI { val chat: Chat } interface Chat { val completions: Completions } interface
* Completions { fun createChatCompletion(...): CreateChatCompletionResponse }
*
* // openAI.chat.completions.createChatCompletion(...)
*/
data class Root(
/* `info.title`, or custom name */
val name: String,
val operations: List<Route>,
val endpoints: List<API>
)

data class API(val name: String, val routes: List<Route>, val nested: List<API>)

private data class RootBuilder(
val name: String,
val operations: MutableList<Route>,
val nested: MutableList<APIBuilder>
) {
fun build(): Root = Root(name, operations, nested.map { it.build() })
}

private data class APIBuilder(
val name: String,
val routes: MutableList<Route>,
val nested: MutableList<APIBuilder>
) {
fun build(): API = API(name, routes, nested.map { it.build() })
}

private object ByPathApiSorter : ApiSorter {
override fun sort(routes: Iterable<Route>): Root {
val root = RootBuilder("OpenAPI", mutableListOf(), mutableListOf())
routes.forEach { route ->
// Reduce paths like `/threads/{thread_id}/runs/{run_id}/submit_tool_outputs`
// into [threads, runs, submit_tool_outputs]
val parts = route.path.replace(Regex("\\{.*?\\}"), "").split("/").filter { it.isNotEmpty() }

val first =
parts.getOrNull(0)
?: run {
// Root operation
root.operations.add(route)
return@forEach
}
// We need to find `chat` in root.operations, and find completions in chat.nested
val api =
root.nested.firstOrNull { it.name == first }
?: run {
val new = APIBuilder(first, mutableListOf(), mutableListOf())
root.nested.add(new)
new
}

addRoute(api, parts.drop(1), route)
}
return root.build()
}

private fun addRoute(builder: APIBuilder, parts: List<String>, route: Route) {
if (parts.isEmpty()) builder.routes.add(route)
else {
val part = parts[0]
val api = builder.nested.firstOrNull { it.name == part }
if (api == null) {
val new = APIBuilder(part, mutableListOf(route), mutableListOf())
builder.nested.add(new)
} else addRoute(api, parts.drop(1), route)
}
}
}

This file was deleted.

Loading

0 comments on commit f056d42

Please sign in to comment.