Skip to content

Commit

Permalink
Generate append infrastructure
Browse files Browse the repository at this point in the history
  • Loading branch information
nomisRev committed Jun 11, 2024
1 parent e25bac6 commit 4366837
Show file tree
Hide file tree
Showing 10 changed files with 103 additions and 66 deletions.
2 changes: 0 additions & 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 Down
44 changes: 0 additions & 44 deletions example/src/commonMain/kotlin/append.kt

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package io.github.nomisrev.openapi
// TODO: make hard-coded.
// We're opinionated about the API structure
internal fun interface ApiSorter {
suspend fun sort(routes: Iterable<Route>): Root
fun sort(routes: Iterable<Route>): Root

companion object {
val ByPath: ApiSorter = ByPathApiSorter
Expand Down Expand Up @@ -46,7 +46,7 @@ private data class APIBuilder(
}

private object ByPathApiSorter : ApiSorter {
override suspend fun sort(routes: Iterable<Route>): Root {
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`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ import io.github.nomisrev.openapi.http.MediaType.Companion.MultipartFormData
import io.github.nomisrev.openapi.http.Method
import io.github.nomisrev.openapi.http.StatusCode

suspend fun OpenAPI.routes(): Root =
OpenAPITransformer(this).routes().let { ApiSorter.ByPath.sort(it) }
fun OpenAPI.routes(): Root = OpenAPITransformer(this).routes().let { ApiSorter.ByPath.sort(it) }

fun OpenAPI.models(): Set<Model> =
with(OpenAPITransformer(this)) { schemas() }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import kotlin.io.path.Path
import okio.FileSystem
import okio.Path.Companion.toPath

suspend fun main() {
val rawSpec = FileSystem.SYSTEM.read("openai.json".toPath()) { readUtf8() }
fun generate(path: String, output: String) {
val rawSpec = FileSystem.SYSTEM.read(path.toPath()) { readUtf8() }
val openAPI = OpenAPI.fromJson(rawSpec)
(openAPI.routes().toFileSpecs() + openAPI.models().toFileSpecs() + predef).forEach {
it.writeTo(Path("../example/build/generated/openapi/src/commonMain/kotlin/"))
it.writeTo(Path(output))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ tailrec fun Model.toTypeSpec(): TypeSpec? =
}

@OptIn(ExperimentalSerializationApi::class)
fun Model.Union.toTypeSpec(): TypeSpec =
private fun Model.Union.toTypeSpec(): TypeSpec =
TypeSpec.interfaceBuilder(Nam.toClassName(context))
.description(description)
.addModifiers(KModifier.SEALED)
Expand Down Expand Up @@ -189,7 +189,7 @@ private fun List<ParameterSpec>.sorted(): List<ParameterSpec> {
* Generating data classes with KotlinPoet!?
* https://stackoverflow.com/questions/44483831/generate-data-class-with-kotlinpoet
*/
fun Model.Object.toTypeSpec(): TypeSpec =
private fun Model.Object.toTypeSpec(): TypeSpec =
TypeSpec.dataClassBuilder(
Nam.toClassName(context),
properties
Expand Down Expand Up @@ -218,7 +218,7 @@ fun Model.Object.toTypeSpec(): TypeSpec =
.addTypes(inline.mapNotNull { it.toTypeSpec() })
.build()

fun Model.Enum.Closed.toTypeSpec(): TypeSpec {
private fun Model.Enum.Closed.toTypeSpec(): TypeSpec {
val rawToName = values.map { rawName -> Pair(rawName, Nam.toEnumValueName(rawName)) }
val isSimple = rawToName.all { (rawName, valueName) -> rawName == valueName }
val enumName = Nam.toClassName(context)
Expand All @@ -245,7 +245,7 @@ fun Model.Enum.Closed.toTypeSpec(): TypeSpec {
.build()
}

fun Model.Enum.Open.toTypeSpec(): TypeSpec {
private fun Model.Enum.Open.toTypeSpec(): TypeSpec {
val rawToName = values.map { rawName -> Pair(rawName, Nam.toEnumValueName(rawName)) }
val enumName = Nam.toClassName(context)
return TypeSpec.interfaceBuilder(enumName)
Expand Down
84 changes: 84 additions & 0 deletions generation/src/jvmMain/kotlin/io/github/nomisrev/openapi/Predef.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package io.github.nomisrev.openapi

import com.squareup.kotlinpoet.AnnotationSpec
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.LambdaTypeName
import com.squareup.kotlinpoet.MemberName
import com.squareup.kotlinpoet.ParameterSpec
import com.squareup.kotlinpoet.ParameterizedTypeName
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
Expand Down Expand Up @@ -43,6 +45,87 @@ private val errors: ParameterizedTypeName =
ClassName("kotlinx.serialization", "SerializationException")
)

private val appendAll: FunSpec =
FunSpec.builder("appendAll")
.addAnnotation(
AnnotationSpec.builder(ClassName("kotlin", "OptIn"))
.addMember("%L::class", "io.ktor.util.InternalAPI")
.build()
)
.addTypeVariable(TypeVariableName("T", Any::class))
.receiver(ClassName("io.ktor.client.request.forms", "FormBuilder"))
.addParameter("key", String::class)
.addParameter("value", TypeVariableName("T").nullable())
.addParameter(
ParameterSpec.builder("headers", ClassName("io.ktor.http", "Headers"))
.defaultValue("%T.Empty", ClassName("io.ktor.http", "Headers"))
.build()
)
.addCode(
"""
when (value) {
is String -> append(key, value, headers)
is Number -> append(key, value, headers)
is Boolean -> append(key, value, headers)
is ByteArray -> append(key, value, headers)
is %T -> append(key, value, headers)
is %T -> append(key, value, headers)
is %T -> append(key, value, headers)
is UploadFile -> appendUploadedFile(key, value)
is Enum<*> -> append(key, serialNameOrEnumValue(value), headers)
null -> Unit
else -> append(key, value, headers)
}
"""
.trimIndent(),
ClassName("io.ktor.utils.io.core", "ByteReadPacket"),
ClassName("io.ktor.client.request.forms", "InputProvider"),
ClassName("io.ktor.client.request.forms", "ChannelProvider")
)
.build()

private val appendUploadedFile: FunSpec =
FunSpec.builder("appendUploadedFile")
.receiver(ClassName("io.ktor.client.request.forms", "FormBuilder"))
.addModifiers(KModifier.PRIVATE)
.addParameter("key", String::class)
.addParameter("file", ClassName(`package`, "UploadFile"))
.addCode(
"""
%M(
key = key,
filename = file.filename,
contentType = file.contentType ?: %T.Application.OctetStream,
size = file.size,
bodyBuilder = file.bodyBuilder
)
"""
.trimIndent(),
MemberName("io.ktor.client.request.forms", "append", isExtension = true),
ContentType
)
.build()

private val serialNameOrEnumValue: FunSpec =
FunSpec.builder("serialNameOrEnumValue")
.addModifiers(KModifier.PRIVATE)
.addAnnotation(
AnnotationSpec.builder(ClassName("kotlin", "OptIn"))
.addMember("%L::class", "kotlinx.serialization.ExperimentalSerializationApi")
.addMember("%L::class", "kotlinx.serialization.InternalSerializationApi")
.build()
)
.addTypeVariable(
TypeVariableName("T", ClassName("kotlin", "Enum").parameterizedBy(TypeVariableName("T")))
)
.returns(String::class)
.addParameter("enum", ClassName("kotlin", "Enum").parameterizedBy(TypeVariableName("T")))
.addCode(
"return enum::class.%M()?.descriptor?.getElementName(enum.ordinal) ?: enum.toString()",
MemberName("kotlinx.serialization", "serializerOrNull", isExtension = true)
)
.build()

val predef: FileSpec =
FileSpec.builder("io.github.nomisrev.openapi", "Predef")
.addType(
Expand Down Expand Up @@ -150,4 +233,5 @@ val predef: FileSpec =
.build()
)
.addType(UploadTypeSpec)
.addFunctions(listOf(appendAll, appendUploadedFile, serialNameOrEnumValue))
.build()
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package io.github.nomisrev.openapi.plugin

import io.github.nomisrev.openapi.generate
import org.gradle.api.DefaultTask
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.plugins.BasePlugin
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction
import org.gradle.api.tasks.options.Option

Expand All @@ -21,13 +21,19 @@ abstract class GenerateClientTask : DefaultTask() {
)
abstract val spec: RegularFileProperty

@get:OutputFile abstract val outputDir: RegularFileProperty

@TaskAction
fun sampleAction() {
val specPath =
requireNotNull(spec.orNull?.asFile?.toPath()?.toString()) {
"No OpenAPI Specification specified. Please provide a spec file."
}
val output =
project.layout.buildDirectory
.dir("generated/openapi/src/commonMain/kotlin")
.get()
.asFile
.also { it.mkdirs() }
.path
generate(specPath, output)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ abstract class OpenAPIPlugin : Plugin<Project> {

project.tasks.register("generateOpenApiClient", GenerateClientTask::class.java) {
it.spec.set(extension.spec)
it.outputDir.set(extension.output)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,9 @@ import javax.inject.Inject
import org.gradle.api.Project
import org.gradle.api.file.RegularFileProperty

const val DEFAULT_OUTPUT_DIR = "generated"

@Suppress("UnnecessaryAbstractClass")
abstract class OpenApiConfig @Inject constructor(project: Project) {
private val objects = project.objects

val spec: RegularFileProperty = objects.fileProperty()

val output: RegularFileProperty =
objects.fileProperty().convention(project.layout.buildDirectory.file(DEFAULT_OUTPUT_DIR))
}

0 comments on commit 4366837

Please sign in to comment.