Skip to content

Commit

Permalink
Add generator option for generating sealed unknowns.
Browse files Browse the repository at this point in the history
  • Loading branch information
christiandeange committed Nov 21, 2024
1 parent 457c025 commit db12002
Show file tree
Hide file tree
Showing 17 changed files with 163 additions and 37 deletions.
1 change: 1 addition & 0 deletions api-gen-runtime/api/api-gen-runtime.api
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,7 @@ public final class sh/christian/ozone/api/runtime/BlobSerializer : kotlinx/seria

public final class sh/christian/ozone/api/runtime/BuildXrpcJsonConfigurationKt {
public static final fun buildXrpcJsonConfiguration (Lkotlinx/serialization/modules/SerializersModule;)Lkotlinx/serialization/json/Json;
public static synthetic fun buildXrpcJsonConfiguration$default (Lkotlinx/serialization/modules/SerializersModule;ILjava/lang/Object;)Lkotlinx/serialization/json/Json;
}

public final class sh/christian/ozone/api/runtime/ImmutableListSerializer : kotlinx/serialization/KSerializer {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import kotlinx.serialization.modules.SerializersModule
/**
* JSON configuration for serializing and deserializing lexicon objects with the given module.
*/
fun buildXrpcJsonConfiguration(module: SerializersModule): Json = Json {
fun buildXrpcJsonConfiguration(module: SerializersModule = Json.serializersModule): Json = Json {
ignoreUnknownKeys = true
classDiscriminator = "${'$'}type"
serializersModule = module
Expand Down
6 changes: 6 additions & 0 deletions bluesky/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ dependencies {
}

lexicons {
namespace.set("sh.christian.ozone.api.xrpc")

defaults {
generateUnknownsForSealedTypes.set(false)
}

generateApi("BlueskyApi") {
packageName.set("sh.christian.ozone")
withKtorImplementation("XrpcBlueskyApi")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ import sh.christian.ozone.api.runtime.buildXrpcJsonConfiguration
/**
* JSON configuration for serializing and deserializing Bluesky API objects.
*/
val BlueskyJson: Json = buildXrpcJsonConfiguration(Json.serializersModule)
val BlueskyJson: Json = buildXrpcJsonConfiguration()
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package sh.christian.ozone.api.generator
import java.io.Serializable

data class ApiConfiguration(
val namespace: String,
val packageName: String,
val interfaceName: String,
val implementationName: String?,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package sh.christian.ozone.api.generator

import java.io.Serializable

data class DefaultsConfiguration(
val generateUnknownsForSealedTypes: Boolean,
) : Serializable
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.squareup.kotlinpoet.CodeBlock
import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.MemberName
import com.squareup.kotlinpoet.ParameterSpec
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import com.squareup.kotlinpoet.PropertySpec
Expand Down Expand Up @@ -219,7 +220,16 @@ class LexiconApiGenerator(
PropertySpec
.builder("client", TypeNames.HttpClient)
.addModifiers(KModifier.PRIVATE)
.initializer(CodeBlock.of("httpClient.%M()", withXrpcConfiguration))
.initializer(
buildCodeBlock {
if (environment.defaults.generateUnknownsForSealedTypes) {
val moduleMemberName = MemberName(configuration.namespace, "XrpcSerializersModule")
add("httpClient.%M(%M)", withXrpcConfiguration, moduleMemberName)
} else {
add("httpClient.%M()", withXrpcConfiguration)
}
}
)
.build()
)
.apply {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
package sh.christian.ozone.api.generator

import com.squareup.kotlinpoet.AnnotationSpec
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.CodeBlock
import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.MemberName
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import com.squareup.kotlinpoet.PropertySpec
import com.squareup.kotlinpoet.TypeSpec.Kind.INTERFACE
import com.squareup.kotlinpoet.TypeVariableName
import com.squareup.kotlinpoet.buildCodeBlock
import com.squareup.kotlinpoet.withIndent
import sh.christian.ozone.api.generator.builder.EnumClass
import sh.christian.ozone.api.generator.builder.EnumEntry
Expand All @@ -31,6 +36,8 @@ class LexiconClassFileCreator(

private val sealedRelationships = mutableListOf<SealedRelationship>()

private val unionTypes = mutableListOf<ClassName>()

fun createClassForLexicon(document: LexiconDocument) {
val enums = mutableMapOf<EnumClass, MutableSet<EnumEntry>>()

Expand All @@ -49,6 +56,14 @@ class LexiconClassFileCreator(
context.typeAliases().forEach { addTypeAlias(it) }

sealedRelationships += context.sealedRelationships()

if (environment.defaults.generateUnknownsForSealedTypes) {
unionTypes += context.types().filter { type ->
type.kind == INTERFACE &&
type.name!!.endsWith("Union") &&
type.typeSpecs.any { it.name == "Unknown" }
}.map { ClassName(context.authority, it.name!!) }
}
}
.addAnnotation(
AnnotationSpec.builder(TypeNames.Suppress)
Expand Down Expand Up @@ -130,4 +145,29 @@ class LexiconClassFileCreator(
.build()
.writeTo(environment.outputDirectory)
}

fun generateSerializerModule(namespace: String) {
if (unionTypes.isEmpty()) return

val xrpcSerializersModuleMemberName = MemberName(namespace, "XrpcSerializersModule")

FileSpec.builder(xrpcSerializersModuleMemberName)
.addProperty(
PropertySpec.builder(xrpcSerializersModuleMemberName.simpleName, TypeNames.SerializersModule)
.initializer(
buildCodeBlock {
beginControlFlow("SerializersModule {")
unionTypes.forEach { unionType ->
beginControlFlow("%M(%T::class) {", polymorphic, unionType)
addStatement("defaultDeserializer { %T.Unknown.serializer() }", unionType)
endControlFlow()
}
endControlFlow()
}
)
.build()
)
.build()
.writeTo(environment.outputDirectory)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import java.io.File

class LexiconProcessingEnvironment(
allLexiconSchemaJsons: List<String>,
val defaults: DefaultsConfiguration,
val outputDirectory: File,
) : Iterable<String> {
private val schemasById: Map<String, String>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ object TypeNames {
val RKey by classOfPackage("sh.christian.ozone.api")
val SerialName by classOfPackage("kotlinx.serialization")
val Serializable by classOfPackage("kotlinx.serialization")
val SerializersModule by classOfPackage("kotlinx.serialization.modules")
val Suppress by classOfPackage("kotlin")
val Tid by classOfPackage("sh.christian.ozone.api")
val Timestamp by classOfPackage("sh.christian.ozone.api.model")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,19 @@ class LexiconDataClassesGenerator(
}
}

if (environment.defaults.generateUnknownsForSealedTypes) {
sealedInterface.addTypes(
createValueClass(
className = name.nestedClass("Unknown"),
innerType = TypeNames.JsonContent,
serialName = null,
additionalConfiguration = {
addSuperinterface(name)
}
),
)
}

context.addType(sealedInterface.build())

return name
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,45 +145,53 @@ fun createDataClass(

fun createValueClass(
className: ClassName,
serialName: String,
serialName: String?,
innerType: TypeName,
additionalConfiguration: TypeSpec.Builder.() -> Unit = {},
): List<TypeSpec> {
val serializerClassName = className.peerClass(className.simpleName + "Serializer")
val serializerTypeSpec = TypeSpec.classBuilder(serializerClassName)
.addSuperinterface(
TypeNames.KSerializer.parameterizedBy(className),
CodeBlock.of(
"""
%M(
⇥serialName = %S,⇤
⇥constructor = ::%T,⇤
⇥valueProvider = %T::value,⇤
⇥valueSerializerProvider = { %T.serializer() },⇤
val serializerTypeSpec = serialName?.let {
TypeSpec.classBuilder(serializerClassName)
.addSuperinterface(
TypeNames.KSerializer.parameterizedBy(className),
CodeBlock.of(
"""
%M(
⇥serialName = %S,⇤
⇥constructor = ::%T,⇤
⇥valueProvider = %T::value,⇤
⇥valueSerializerProvider = { %T.serializer() },⇤
)
""".trimIndent(),
valueClassSerializer,
serialName,
className,
className,
innerType,
)
""".trimIndent(),
valueClassSerializer,
serialName,
className,
className,
innerType,
)
)
.build()
.build()
}

val valueClassTypeSpec = TypeSpec.classBuilder(className)
.addAnnotation(
AnnotationSpec.builder(TypeNames.Serializable)
.addMember("with = %T::class", serializerClassName)
.build()
)
.addModifiers(KModifier.VALUE)
.addAnnotation(JvmInline::class)
.addAnnotation(
AnnotationSpec.builder(TypeNames.SerialName)
.addMember("%S", serialName)
.build()
)
.apply {
if (serialName == null) {
addAnnotation(TypeNames.Serializable)
} else {
addAnnotation(
AnnotationSpec.builder(TypeNames.Serializable)
.addMember("with = %T::class", serializerClassName)
.build()
)
addAnnotation(
AnnotationSpec.builder(TypeNames.SerialName)
.addMember("%S", serialName)
.build()
)
}
}
.primaryConstructor(
FunSpec.constructorBuilder()
.addParameter(
Expand All @@ -202,7 +210,7 @@ fun createValueClass(
.apply(additionalConfiguration)
.build()

return listOf(serializerTypeSpec, valueClassTypeSpec)
return listOfNotNull(serializerTypeSpec, valueClassTypeSpec)
}

fun createObjectClass(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ val findSubscriptionSerializer by memberOfPackage("sh.christian.ozone.api.xrpc")

val persistentListOf by memberOfPackage("kotlinx.collections.immutable")

val polymorphic by extensionMemberOfPackage("kotlinx.serialization.modules")

val procedure by extensionMemberOfPackage("sh.christian.ozone.api.xrpc")

val query by extensionMemberOfPackage("sh.christian.ozone.api.xrpc")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import org.intellij.lang.annotations.Language
import sh.christian.ozone.api.generator.ApiConfiguration
import sh.christian.ozone.api.generator.ApiReturnType
import sh.christian.ozone.api.generator.ApiReturnType.Raw
import sh.christian.ozone.api.generator.DefaultsConfiguration
import javax.inject.Inject

abstract class LexiconGeneratorExtension
Expand All @@ -21,24 +22,46 @@ abstract class LexiconGeneratorExtension
internal val apiConfigurations: ListProperty<ApiConfiguration> =
objects.listProperty<ApiConfiguration>().convention(emptyList())

val namespace: Property<String> =
objects.property<String>().convention("sh.christian.ozone")

internal val defaults = GeneratorDefaults(objects)

val outputDirectory: DirectoryProperty =
objects.directoryProperty().convention(
projectLayout.buildDirectory
.map { it.dir("generated") }
.map { it.dir("lexicons") }
)

fun defaults(configure: GeneratorDefaults.() -> Unit) {
defaults.configure()
}

fun generateApi(
name: String,
configure: ApiGeneratorExtension.() -> Unit = {},
) {
apiConfigurations.add(
ApiGeneratorExtension(name, objects)
.apply(configure)
.apiConfiguration
.buildApiConfiguration(namespace.readFinalizedValue())
)
}

class GeneratorDefaults internal constructor(
objects: ObjectFactory,
) {
val generateUnknownsForSealedTypes: Property<Boolean> =
objects.property<Boolean>().convention(false)

internal fun buildDefaultsConfiguration(): DefaultsConfiguration {
return DefaultsConfiguration(
generateUnknownsForSealedTypes = generateUnknownsForSealedTypes.readFinalizedValue(),
)
}
}

class ApiGeneratorExtension internal constructor(
internal val name: String,
objects: ObjectFactory,
Expand Down Expand Up @@ -73,8 +96,9 @@ abstract class LexiconGeneratorExtension
implementationName.set(name)
}

internal val apiConfiguration by lazy {
ApiConfiguration(
internal fun buildApiConfiguration(namespace: String): ApiConfiguration {
return ApiConfiguration(
namespace = namespace,
packageName = packageName.readFinalizedValue(),
interfaceName = name,
implementationName = implementationName.readFinalizedValueOrNull(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ private fun Project.applyPlugin() {

val generateLexicons = tasks.register<LexiconGeneratorTask>("generateLexicons") {
schemasClasspath.from(configuration)
namespace.set(extension.namespace)
defaults.set(extension.defaults.buildDefaultsConfiguration())
apiConfigurations.set(extension.apiConfigurations)
outputDirectory.set(extension.outputDirectory)
}
Expand Down
Loading

0 comments on commit db12002

Please sign in to comment.