Skip to content

Commit

Permalink
Update Gradle Config & Support YAML format for Gradle (#37)
Browse files Browse the repository at this point in the history
  • Loading branch information
nomisRev authored Jun 26, 2024
1 parent 9ecebd8 commit 7ec5fe5
Show file tree
Hide file tree
Showing 34 changed files with 9,491 additions and 278 deletions.
10 changes: 5 additions & 5 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ configure<PowerAssertGradleExtension> {

subprojects {
tasks {
withType(Jar::class.java) {
manifest {
attributes("Automatic-Module-Name" to "io.github.nomisrev")
}
}
// withType(Jar::class.java) {
// manifest {
// attributes("Automatic-Module-Name" to "io.github.nomisrev")
// }
// }
withType<JavaCompile> {
options.release.set(8)
}
Expand Down
2 changes: 1 addition & 1 deletion generation/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ kotlin {
}

dependencies {
implementation(libs.okio)
api(libs.okio)
api(libs.ktor.client)
api(projects.typed)
api(libs.kasechange)
Expand Down
86 changes: 40 additions & 46 deletions generation/src/main/kotlin/io/github/nomisrev/openapi/APIs.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ import com.squareup.kotlinpoet.LambdaTypeName
import com.squareup.kotlinpoet.MemberName
import com.squareup.kotlinpoet.ParameterSpec
import com.squareup.kotlinpoet.PropertySpec
import com.squareup.kotlinpoet.TypeName
import com.squareup.kotlinpoet.TypeSpec
import com.squareup.kotlinpoet.asTypeName
import com.squareup.kotlinpoet.withIndent
import io.github.nomisrev.openapi.NamingContext.Named
import io.github.nomisrev.openapi.NamingContext.Nested
import io.ktor.http.*

fun configure(defaults: Boolean) =
ParameterSpec.builder(
Expand Down Expand Up @@ -50,11 +50,15 @@ private fun Root.apis(): List<FileSpec> =
.build()
}

context(OpenAPIContext)
private fun Root.className(): ClassName =
toClassName(Named(name))

context(OpenAPIContext)
private fun Root.root() =
FileSpec.builder(`package`, "OpenAPI")
FileSpec.builder(`package`, className().simpleName)
.addType(
TypeSpec.interfaceBuilder("OpenAPI")
TypeSpec.interfaceBuilder(className())
.addProperties(
endpoints.map { api ->
PropertySpec.builder(toParamName(Named(api.name)), toClassName(Named(api.name)))
Expand All @@ -65,16 +69,16 @@ private fun Root.root() =
)
.addFunctions(operations.map { it.toFun(implemented = false) })
.addFunction(
FunSpec.builder("OpenAPI")
FunSpec.builder(className().simpleName)
.addParameter("client", ClassName("io.ktor.client", "HttpClient"))
.addStatement("return %T(client)", ClassName(`package`, "OpenAPIKtor"))
.returns(ClassName(`package`, "OpenAPI"))
.addStatement("return %T(client)", className().postfix("Ktor"))
.returns(className())
.build()
)
.addType(
TypeSpec.classBuilder(ClassName(`package`, "OpenAPIKtor"))
TypeSpec.classBuilder(className().postfix("Ktor"))
.addModifiers(KModifier.PRIVATE)
.addSuperinterface(ClassName(`package`, "OpenAPI"))
.addSuperinterface(className())
.apiConstructor()
.addProperties(
endpoints.map { api ->
Expand Down Expand Up @@ -102,39 +106,13 @@ private fun TypeSpec.Builder.apiConstructor(): TypeSpec.Builder =
.build()
)

context(OpenAPIContext)
private fun Route.nestedTypes(): List<TypeSpec> = inputs() + returns() + bodies()

context(OpenAPIContext)
private fun Route.inputs(): List<TypeSpec> =
input.mapNotNull { it.type.toTypeSpecOrNull() }

context(OpenAPIContext)
private fun Route.returns(): List<TypeSpec> =
returnType.types.values.mapNotNull { it.type.toTypeSpecOrNull() }

context(OpenAPIContext)
private fun Route.bodies(): List<TypeSpec> =
body.types.values.flatMap { body ->
when (body) {
is Route.Body.Json.Defined ->
listOfNotNull(body.type.toTypeSpecOrNull())

is Route.Body.Multipart.Value -> body.parameters.mapNotNull { it.type.toTypeSpecOrNull() }
is Route.Body.Multipart.Ref,
is Route.Body.Xml,
is Route.Body.Json.FreeForm,
is Route.Body.OctetStream -> emptyList()
}
}

context(OpenAPIContext)
private fun API.toInterface(outerContext: NamingContext? = null): TypeSpec {
val outer = outerContext?.let { Nested(Named(name), it) } ?: Named(name)
val nested = nested.map { intercept(it) }
val typeSpec = TypeSpec.interfaceBuilder(toClassName(outer))
.addFunctions(routes.map { it.toFun(implemented = false) })
.addTypes(routes.flatMap { it.nestedTypes() })
.addTypes(routes.flatMap { r -> r.nested.mapNotNull { it.toTypeSpecOrNull() } })
.addTypes(nested.map { it.toInterface(outer) })
.addProperties(
nested.map {
Expand All @@ -158,7 +136,6 @@ private fun API.toImplementationClassName(namingContext: NamingContext): ClassNa

context(OpenAPIContext)
private fun API.toImplementation(outerContext: NamingContext? = null): TypeSpec {

fun ClassName.toContext(): NamingContext {
val names = simpleNames
return when (names.size) {
Expand Down Expand Up @@ -204,7 +181,7 @@ private fun Route.toFun(implemented: Boolean): FunSpec =
.addParameters(params(defaults = !implemented))
.addParameters(requestBody(defaults = !implemented))
.addParameter(configure(defaults = !implemented))
.returns(returnType())
.returnType()
.apply {
if (implemented) {
addCode(
Expand Down Expand Up @@ -349,12 +326,29 @@ fun Route.requestBody(defaults: Boolean): List<ParameterSpec> {
}

// TODO generate an ADT to properly support all return types
context(OpenAPIContext)
fun Route.returnType(): TypeName {
val success =
returnType.types.toSortedMap { s1, s2 -> s1.compareTo(s2) }.entries.first()
return when (success.value.type) {
is Model.OctetStream -> HttpResponse
else -> success.value.type.toTypeName()
}
}
context(OpenAPIContext, Route)
fun FunSpec.Builder.returnType(): FunSpec.Builder =
if (returnType.types.size == 1) {
val single = returnType.types.entries.single()
val typeName = when (single.value.type) {
is Model.OctetStream -> HttpResponse
else -> single.value.type.toTypeName()
}
returns(typeName)
} else {
val response = ClassName(`package`, "${operation.operationId}Response")
// TODO Add to FileSpec
TypeSpec.interfaceBuilder(response)
.addModifiers(KModifier.SEALED)
.addTypes(
returnType.types.map { (status: HttpStatusCode, type) ->
val case = ClassName(`package`, status.description.split(" ").joinToString(""))
TypeSpec.dataClassBuilder(
case,
listOf(ParameterSpec.builder("value", type.type.toTypeName()).build())
).build()
}
)
.build()
returns(response)
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,12 @@ private fun List<ParameterSpec>.sorted(): List<ParameterSpec> {
fun ClassName.nested(name: String): ClassName =
ClassName(packageName, simpleName, name)

fun ClassName.postfix(postfix: String): ClassName =
ClassName(
packageName,
simpleNames.dropLast(1) + (simpleNames.last() + postfix)
)

val ContentType = ClassName("io.ktor.http", "ContentType")
val HttpResponse = ClassName("io.ktor.client.statement", "HttpResponse")
val SerialDescriptor = ClassName("kotlinx.serialization.descriptors", "SerialDescriptor")
Expand Down
37 changes: 31 additions & 6 deletions generation/src/main/kotlin/io/github/nomisrev/openapi/Main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,40 @@ import okio.FileSystem
import okio.Path.Companion.toPath
import kotlin.io.path.Path

fun generate(path: String, output: String) {
val rawSpec = FileSystem.SYSTEM.read(path.toPath()) { readUtf8() }
val openAPI = OpenAPI.fromJson(rawSpec)
with(OpenAPIContext("io.github.nomisrev.openapi")) {
val root = openAPI.root()
fun main() {
generate(
GenerationConfig(
"openai-api.yaml",
"generation/build/geneated",
"io.github.nomisrev.openapi",
"OpenAI"
)
)
}

data class GenerationConfig(
val path: String,
val output: String,
val `package`: String,
val name: String
)

@JvmOverloads
fun generate(config: GenerationConfig, fileSystem: FileSystem = FileSystem.SYSTEM) {
val rawSpec = fileSystem.read(config.path.toPath()) { readUtf8() }
val openAPI = when (val extension = config.path.substringAfterLast(".")) {
"json" -> OpenAPI.fromJson(rawSpec)
"yaml",
"yml" -> OpenAPI.fromYaml(rawSpec)

else -> throw IllegalArgumentException("Unsupported file extension: $extension")
}
with(OpenAPIContext(config.`package`)) {
val root = openAPI.root(config.name)
val models = openAPI.models()
val modelFileSpecs = models.toFileSpecs()
val rootFileSpecs = root.toFileSpecs()
val files = rootFileSpecs + modelFileSpecs + predef() + additionalFiles()
files.forEach { it.writeTo(Path(output)) }
files.forEach { it.writeTo(Path(config.output)) }
}
}
19 changes: 17 additions & 2 deletions generation/src/main/kotlin/io/github/nomisrev/openapi/Models.kt
Original file line number Diff line number Diff line change
Expand Up @@ -242,20 +242,35 @@ private fun Model.Enum.Closed.toTypeSpec(): TypeSpec {
return TypeSpec.enumBuilder(enumName)
.description(description)
.apply {
if (!isSimple)
if (!isSimple) {
primaryConstructor(FunSpec.constructorBuilder().addParameter("value", STRING).build())
addProperty(
PropertySpec.builder("value", STRING)
.initializer("value")
.build()
)
}
rawToName.forEach { (rawName, valueName) ->
if (isSimple) addEnumConstant(rawName)
else
else {
addEnumConstant(
valueName,
TypeSpec.anonymousClassBuilder()
.addAnnotation(
annotationSpec<SerialName>().toBuilder().addMember("\"$rawName\"").build()
)
.apply {
// If we're `9...` we need to drop backticks, and prefix with `_`
if (Regex("`[0-9]").matchesAt(valueName, 0)) addAnnotation(
AnnotationSpec.builder(ClassName("kotlin.js", "JsName"))
.addMember("\"_${valueName.drop(1).dropLast(1)}\"")
.build()
)
}
.addSuperclassConstructorParameter("\"$rawName\"")
.build()
)
}
}
}
.addAnnotation(annotationSpec<Serializable>())
Expand Down
13 changes: 11 additions & 2 deletions generation/src/main/kotlin/io/github/nomisrev/openapi/Naming.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ package io.github.nomisrev.openapi

import com.squareup.kotlinpoet.ClassName
import io.github.nomisrev.openapi.Model.Collection
import net.pearx.kasechange.splitToWords
import net.pearx.kasechange.splitter.WordSplitterConfig
import net.pearx.kasechange.splitter.WordSplitterConfigurable
import net.pearx.kasechange.toCamelCase
import net.pearx.kasechange.toPascalCase
import java.util.*

fun Naming(`package`: String): Naming = Nam(`package`)

Expand All @@ -29,9 +31,16 @@ private class Nam(private val `package`: String) : Naming {
)
)

private fun String.toPascalCase(): String = toPascalCase(wordSplitter)
private fun String.toPascalCase(): String =
splitToWords(wordSplitter).joinToString("") { word ->
word.replaceFirstChar {
if (it.isLowerCase()) it.titlecase()
else it.toString()
}
}

private fun String.toCamelCase(): String = toCamelCase(wordSplitter)
private fun String.toCamelCase(): String =
toPascalCase().replaceFirstChar { it.lowercase() }

override fun toClassName(context: NamingContext): ClassName =
when (context) {
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g

GROUP=io.github.nomisrev
SONATYPE_HOST=S01
RELEASE_SIGNING_ENABLED=true
#RELEASE_SIGNING_ENABLED=true

POM_URL=https://github.com/nomisrev/OpenAPI-kt/

Expand Down
7 changes: 5 additions & 2 deletions parser/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,16 @@ kotlin {
explicitApi()

jvm()
macosArm64()
linuxX64()
// macosArm64()
// linuxX64()

sourceSets {
commonMain {
dependencies {
api(libs.json)
// This should be KAML, but parsing OpenAI takes 57seconds
// Compared to 100ms with SnakeYAML
implementation("org.yaml:snakeyaml:2.1")
}
}
jvmTest {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package io.github.nomisrev.openapi

import io.github.nomisrev.openapi.OpenAPI.Companion.Json
import kotlin.jvm.JvmInline
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerializationException
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.JsonDecoder
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
Expand All @@ -27,11 +27,14 @@ public sealed interface AdditionalProperties {
buildClassSerialDescriptor("io.github.nomisrev.openapi.AdditionalProperties")

override fun deserialize(decoder: Decoder): AdditionalProperties {
decoder as JsonDecoder
val json = decoder.decodeSerializableValue(JsonElement.serializer())
return when {
json is JsonPrimitive && json.booleanOrNull != null -> Allowed(json.boolean)
json is JsonObject ->
PSchema(Json.decodeFromJsonElement(ReferenceOr.serializer(Schema.serializer()), json))
PSchema(
decoder.json.decodeFromJsonElement(ReferenceOr.serializer(Schema.serializer()), json)
)
else ->
throw SerializationException("AdditionalProperties can only be a boolean or a schema")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ public data class Components(
public companion object {
internal object Serializer :
KSerializerWithExtensions<Components>(
OpenAPI.Json,
serializer(),
Components::extensions,
{ op, extensions -> op.copy(extensions = extensions) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ public data class Encoding(
public companion object {
internal object Serializer :
KSerializerWithExtensions<Encoding>(
OpenAPI.Json,
serializer(),
Encoding::extensions,
{ op, extensions -> op.copy(extensions = extensions) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ public data class Example(
public companion object {
internal object Serializer :
KSerializerWithExtensions<Example>(
OpenAPI.Json,
serializer(),
Example::extensions,
{ op, extensions -> op.copy(extensions = extensions) }
Expand Down
Loading

0 comments on commit 7ec5fe5

Please sign in to comment.