Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: total rehaul #15

Draft
wants to merge 15 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,7 @@ build
.idea

# Codegen Playground
playground/src/main/gen
playground/src/main/gen

# Dotenv
.env
7 changes: 7 additions & 0 deletions codegen/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import org.gradle.kotlin.dsl.detekt
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
kotlin("jvm")
Expand All @@ -12,6 +13,12 @@ plugins {
id("signing")
}

tasks.withType<KotlinCompile>().configureEach {
kotlinOptions {
freeCompilerArgs = freeCompilerArgs + "-Xcontext-receivers"
}
}

detekt {
autoCorrect = true
}
Expand Down
5 changes: 5 additions & 0 deletions codegen/src/main/kotlin/io/bkbn/skribe/codegen/App.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package io.bkbn.skribe.codegen

fun main() {
Skribe.generate("/alpaca-broker.yml", "com.alpaca.client")
}
30 changes: 30 additions & 0 deletions codegen/src/main/kotlin/io/bkbn/skribe/codegen/Skribe.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package io.bkbn.skribe.codegen

import com.squareup.kotlinpoet.FileSpec
import io.bkbn.skribe.codegen.converter.ConverterMetadata
import io.bkbn.skribe.codegen.converter.SpecConverter
import io.bkbn.skribe.codegen.generator.EnumGenerator
import io.bkbn.skribe.codegen.generator.ModelGenerator
import io.bkbn.skribe.codegen.generator.RequestGenerator
import io.bkbn.skribe.codegen.generator.SerializerGenerator
import io.bkbn.skribe.codegen.generator.TypeAliasGenerator
import io.swagger.parser.OpenAPIParser

object Skribe {
fun generate(specUrl: String, rootPackage: String): List<FileSpec> {
val spec = OpenAPIParser().readLocation(specUrl, null, null)
val metadata = ConverterMetadata(rootPackage)

val skribeSpec = with(metadata) { SpecConverter.convert(spec.openAPI) }

return with(skribeSpec) {
val enums = EnumGenerator.generate()
val models = ModelGenerator.generate()
val serializers = SerializerGenerator.generate()
val requests = RequestGenerator.generate()
val typeAliases = TypeAliasGenerator.generate()

enums + models + serializers + requests + typeAliases
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package io.bkbn.skribe.codegen.converter

sealed interface Converter<in T, out R> {
context(ConverterMetadata)
fun convert(input: T): R
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package io.bkbn.skribe.codegen.converter

data class ConverterMetadata(
val rootPackage: String,
val currentPackage: String = rootPackage,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package io.bkbn.skribe.codegen.converter

import io.bkbn.skribe.codegen.domain.SkribeParameter
import io.bkbn.skribe.codegen.domain.SkribeParameterLiteral
import io.bkbn.skribe.codegen.domain.SkribeParameterReference
import io.swagger.v3.oas.models.parameters.Parameter

data object ParameterConverter : Converter<Map<String, Parameter>, List<SkribeParameter>> {

context(ConverterMetadata)
override fun convert(input: Map<String, Parameter>): List<SkribeParameter> = input.map { (name, parameter) ->
when (parameter.`$ref`) {
null -> SkribeParameterLiteral(
name = parameter.name?.let { SkribeParameterLiteral.ParameterName(it) },
componentName = name,
description = parameter.description,
`in` = parameter.`in`?.toSkribeParameterIn() ?: error("Parameter `$name` has no `in` value"),
required = parameter.required,
deprecated = parameter.deprecated ?: false,
)

else -> SkribeParameterReference(
ref = parameter.`$ref`
)
}
}

private fun String.toSkribeParameterIn(): SkribeParameter.In = when (this) {
"header" -> SkribeParameter.In.HEADER
"query" -> SkribeParameter.In.QUERY
"path" -> SkribeParameter.In.PATH
else -> error("Unknown parameter type: $this")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package io.bkbn.skribe.codegen.converter

import io.bkbn.skribe.codegen.domain.SkribePath
import io.bkbn.skribe.codegen.utils.StringUtils.capitalized
import io.swagger.v3.oas.models.Operation
import io.swagger.v3.oas.models.PathItem
import io.swagger.v3.oas.models.parameters.Parameter

data object PathConverter : Converter<Map<String, PathItem>, List<SkribePath>> {

context(ConverterMetadata)
override fun convert(input: Map<String, PathItem>): List<SkribePath> = input.map { (path, item) ->
val paths = mutableListOf<SkribePath>()
val pathParams = item.parameters ?: emptyList()
item.get?.let { paths.add(createOperation(SkribePath.Operation.GET, path, it, pathParams)) }
item.put?.let { paths.add(createOperation(SkribePath.Operation.PUT, path, it, pathParams)) }
item.post?.let { paths.add(createOperation(SkribePath.Operation.POST, path, it, pathParams)) }
item.delete?.let { paths.add(createOperation(SkribePath.Operation.DELETE, path, it, pathParams)) }
item.patch?.let { paths.add(createOperation(SkribePath.Operation.PATCH, path, it, pathParams)) }
item.head?.let { paths.add(createOperation(SkribePath.Operation.HEAD, path, it, pathParams)) }
item.options?.let { paths.add(createOperation(SkribePath.Operation.OPTIONS, path, it, pathParams)) }
item.trace?.let { paths.add(createOperation(SkribePath.Operation.TRACE, path, it, pathParams)) }
paths
}.flatten()

context(ConverterMetadata)
private fun createOperation(
operationType: SkribePath.Operation,
path: String,
operation: Operation,
pathParameters: List<Parameter>,
): SkribePath {
return SkribePath(
path = path,
operation = operationType,
name = SkribePath.PathName(
operation.operationId
?: operation.summary.split(" ")
.filterNot { it.isEmpty() }
.joinToString("") {
it.capitalized()
}
),
description = operation.description,
requestBody = operation.requestBody?.let { RequestBodyConverter.convert(it) },
responses = operation.responses?.let { ResponseConverter.convert(it) } ?: emptyList(),
pathParameters = pathParameters.associateBy { it.name ?: it.`$ref` }.let { ParameterConverter.convert(it) },
operationParameters = operation.parameters?.associateBy { it.name ?: it.`$ref` }?.let {
ParameterConverter.convert(
it
)
}
?: emptyList(),
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package io.bkbn.skribe.codegen.converter

import io.bkbn.skribe.codegen.domain.SkribeRequest
import io.swagger.v3.oas.models.parameters.RequestBody

data object RequestBodyConverter : Converter<RequestBody, SkribeRequest> {
context(ConverterMetadata)
override fun convert(input: RequestBody): SkribeRequest = SkribeRequest(
required = input.required ?: false,
schema = SchemaConverter.convert(mapOf("blah" to input.content.values.first().schema)).first(),
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package io.bkbn.skribe.codegen.converter

import io.bkbn.skribe.codegen.domain.SkribeResponse
import io.swagger.v3.oas.models.responses.ApiResponse

data object ResponseConverter : Converter<Map<String, ApiResponse>, List<SkribeResponse>> {

context(ConverterMetadata)
override fun convert(input: Map<String, ApiResponse>): List<SkribeResponse> = input.map { (name, response) ->
SkribeResponse(
name = name.toIntOrNull()?.let { name },
description = response.description,
schema = response.content?.let { SchemaConverter.convert(mapOf("blah" to it.values.first().schema)).first() },
statusCode = name.toIntOrNull(),
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package io.bkbn.skribe.codegen.converter

import io.bkbn.skribe.codegen.domain.schema.SkribeArraySchema
import io.bkbn.skribe.codegen.domain.schema.SkribeBooleanSchema
import io.bkbn.skribe.codegen.domain.schema.SkribeComposedSchema
import io.bkbn.skribe.codegen.domain.schema.SkribeDateSchema
import io.bkbn.skribe.codegen.domain.schema.SkribeDateTimeSchema
import io.bkbn.skribe.codegen.domain.schema.SkribeEmailSchema
import io.bkbn.skribe.codegen.domain.schema.SkribeEnumSchema
import io.bkbn.skribe.codegen.domain.schema.SkribeFreeFormSchema
import io.bkbn.skribe.codegen.domain.schema.SkribeIntegerSchema
import io.bkbn.skribe.codegen.domain.schema.SkribeNumberSchema
import io.bkbn.skribe.codegen.domain.schema.SkribeObjectSchema
import io.bkbn.skribe.codegen.domain.schema.SkribeReferenceSchema
import io.bkbn.skribe.codegen.domain.schema.SkribeSchema
import io.bkbn.skribe.codegen.domain.schema.SkribeStringSchema
import io.bkbn.skribe.codegen.domain.schema.SkribeUuidSchema
import io.bkbn.skribe.codegen.utils.StringUtils.convertToPascalCase
import io.bkbn.skribe.codegen.utils.StringUtils.getRefKey
import io.swagger.v3.oas.models.media.ArraySchema
import io.swagger.v3.oas.models.media.BooleanSchema
import io.swagger.v3.oas.models.media.ComposedSchema
import io.swagger.v3.oas.models.media.DateSchema
import io.swagger.v3.oas.models.media.DateTimeSchema
import io.swagger.v3.oas.models.media.EmailSchema
import io.swagger.v3.oas.models.media.IntegerSchema
import io.swagger.v3.oas.models.media.NumberSchema
import io.swagger.v3.oas.models.media.ObjectSchema
import io.swagger.v3.oas.models.media.Schema
import io.swagger.v3.oas.models.media.StringSchema
import io.swagger.v3.oas.models.media.UUIDSchema

data object SchemaConverter : Converter<Map<String, Schema<*>>, List<SkribeSchema>> {

context(ConverterMetadata)
override fun convert(
input: Map<String, Schema<*>>,
): List<SkribeSchema> = input.map { (name, schema) ->
when (schema) {
is ArraySchema -> schema.toSkribeArraySchema(name)
is BooleanSchema -> schema.toSkribeBooleanSchema(name)
is ComposedSchema -> schema.toSkribeComposedSchema(name)
is ObjectSchema -> when {
schema.properties == null -> schema.toFreeFormSchema(name)
else -> schema.toSkribeObjectSchema(name)
}

is IntegerSchema -> schema.toSkribeIntegerSchema(name)
is DateSchema -> schema.toSkribeDateSchema(name)
is DateTimeSchema -> schema.toSkribeDateTimeSchema(name)
is NumberSchema -> schema.toSkribeNumberSchema(name)
is UUIDSchema -> schema.toSkribeUuidSchema(name)
is EmailSchema -> schema.toSkribeEmailSchema(name)
is StringSchema -> {
if (schema.enum != null) return@map schema.toSkribeEnumSchema(name)

schema.toSkribeStringSchema(name)
}

else -> when {
schema.`$ref` != null -> schema.toSkribeReferenceSchema(name)
else -> error("Unknown schema type: $schema")
}
}
}

context(ConverterMetadata)
private fun ObjectSchema.toSkribeObjectSchema(name: String): SkribeObjectSchema = SkribeObjectSchema(
name = name,
required = required ?: emptyList(),
properties = properties?.let {
val updatedMetadata = ConverterMetadata(
rootPackage = rootPackage,
currentPackage = name.convertToPascalCase() // TODO: Use addressable name
)
with(updatedMetadata) { convert(it).associateBy { s -> SkribeObjectSchema.PropertyName(s.name) } }
} ?: error("Schema $name does not have properties, should be registered as a FreeFormSchema"),
currentPackage = currentPackage,
)

context(ConverterMetadata)
private fun StringSchema.toSkribeEnumSchema(name: String): SkribeEnumSchema = SkribeEnumSchema(
name = name,
values = enum?.map { SkribeEnumSchema.SkribeEnumValue(it) } ?: error("Schema $name is not an enum type."),
currentPackage = currentPackage,
)

private fun ComposedSchema.toSkribeComposedSchema(name: String): SkribeComposedSchema = SkribeComposedSchema(
name = name,
)

private fun StringSchema.toSkribeStringSchema(name: String): SkribeStringSchema = SkribeStringSchema(
name = name,
)

context(ConverterMetadata)
private fun ArraySchema.toSkribeArraySchema(name: String): SkribeArraySchema = SkribeArraySchema(
name = name,
items = convert(mapOf("items" to items)).first()
)

context(ConverterMetadata)
private fun UUIDSchema.toSkribeUuidSchema(name: String): SkribeUuidSchema = SkribeUuidSchema(
name = name,
utilPackage = rootPackage.plus(".util")
)

private fun Schema<*>.toSkribeReferenceSchema(name: String): SkribeReferenceSchema = SkribeReferenceSchema(
name = name,
ref = this.`$ref`.getRefKey()
)

private fun DateSchema.toSkribeDateSchema(name: String): SkribeDateSchema = SkribeDateSchema(
name = name,
)

private fun DateTimeSchema.toSkribeDateTimeSchema(name: String): SkribeDateTimeSchema = SkribeDateTimeSchema(
name = name,
)

private fun BooleanSchema.toSkribeBooleanSchema(name: String): SkribeBooleanSchema = SkribeBooleanSchema(
name = name,
)

private fun IntegerSchema.toSkribeIntegerSchema(name: String): SkribeIntegerSchema = SkribeIntegerSchema(
name = name,
)

private fun EmailSchema.toSkribeEmailSchema(name: String): SkribeEmailSchema = SkribeEmailSchema(
name = name,
)

context(ConverterMetadata)
private fun NumberSchema.toSkribeNumberSchema(name: String): SkribeNumberSchema = SkribeNumberSchema(
name = name,
utilPackage = rootPackage.plus(".util")
)

private fun ObjectSchema.toFreeFormSchema(name: String): SkribeFreeFormSchema = SkribeFreeFormSchema(
name = name,
)
}
Loading
Loading