diff --git a/api-gen-runtime/api/api-gen-runtime.api b/api-gen-runtime/api/api-gen-runtime.api index 99dcdd8..06c2125 100644 --- a/api-gen-runtime/api/api-gen-runtime.api +++ b/api-gen-runtime/api/api-gen-runtime.api @@ -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 { diff --git a/api-gen-runtime/src/commonMain/kotlin/sh/christian/ozone/api/runtime/buildXrpcJsonConfiguration.kt b/api-gen-runtime/src/commonMain/kotlin/sh/christian/ozone/api/runtime/buildXrpcJsonConfiguration.kt index 7bfd9c0..c06cbc6 100644 --- a/api-gen-runtime/src/commonMain/kotlin/sh/christian/ozone/api/runtime/buildXrpcJsonConfiguration.kt +++ b/api-gen-runtime/src/commonMain/kotlin/sh/christian/ozone/api/runtime/buildXrpcJsonConfiguration.kt @@ -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 diff --git a/bluesky/build.gradle.kts b/bluesky/build.gradle.kts index e0baed5..6f6a569 100644 --- a/bluesky/build.gradle.kts +++ b/bluesky/build.gradle.kts @@ -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") diff --git a/bluesky/src/commonMain/kotlin/sh/christian/ozone/BlueskyJson.kt b/bluesky/src/commonMain/kotlin/sh/christian/ozone/BlueskyJson.kt index eaccebb..62247ba 100644 --- a/bluesky/src/commonMain/kotlin/sh/christian/ozone/BlueskyJson.kt +++ b/bluesky/src/commonMain/kotlin/sh/christian/ozone/BlueskyJson.kt @@ -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() diff --git a/generator/src/main/kotlin/sh/christian/ozone/api/generator/ApiConfiguration.kt b/generator/src/main/kotlin/sh/christian/ozone/api/generator/ApiConfiguration.kt index 215a5b6..071495e 100644 --- a/generator/src/main/kotlin/sh/christian/ozone/api/generator/ApiConfiguration.kt +++ b/generator/src/main/kotlin/sh/christian/ozone/api/generator/ApiConfiguration.kt @@ -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?, diff --git a/generator/src/main/kotlin/sh/christian/ozone/api/generator/DefaultsConfiguration.kt b/generator/src/main/kotlin/sh/christian/ozone/api/generator/DefaultsConfiguration.kt new file mode 100644 index 0000000..52b0f61 --- /dev/null +++ b/generator/src/main/kotlin/sh/christian/ozone/api/generator/DefaultsConfiguration.kt @@ -0,0 +1,7 @@ +package sh.christian.ozone.api.generator + +import java.io.Serializable + +data class DefaultsConfiguration( + val generateUnknownsForSealedTypes: Boolean, +) : Serializable diff --git a/generator/src/main/kotlin/sh/christian/ozone/api/generator/LexiconApiGenerator.kt b/generator/src/main/kotlin/sh/christian/ozone/api/generator/LexiconApiGenerator.kt index 5acd44a..b237cff 100644 --- a/generator/src/main/kotlin/sh/christian/ozone/api/generator/LexiconApiGenerator.kt +++ b/generator/src/main/kotlin/sh/christian/ozone/api/generator/LexiconApiGenerator.kt @@ -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 @@ -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 { diff --git a/generator/src/main/kotlin/sh/christian/ozone/api/generator/LexiconClassFileCreator.kt b/generator/src/main/kotlin/sh/christian/ozone/api/generator/LexiconClassFileCreator.kt index a464618..e8bec90 100644 --- a/generator/src/main/kotlin/sh/christian/ozone/api/generator/LexiconClassFileCreator.kt +++ b/generator/src/main/kotlin/sh/christian/ozone/api/generator/LexiconClassFileCreator.kt @@ -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 @@ -31,6 +36,8 @@ class LexiconClassFileCreator( private val sealedRelationships = mutableListOf() + private val unionTypes = mutableListOf() + fun createClassForLexicon(document: LexiconDocument) { val enums = mutableMapOf>() @@ -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) @@ -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) + } } diff --git a/generator/src/main/kotlin/sh/christian/ozone/api/generator/LexiconProcessingEnvironment.kt b/generator/src/main/kotlin/sh/christian/ozone/api/generator/LexiconProcessingEnvironment.kt index 8289cb6..e970c3a 100644 --- a/generator/src/main/kotlin/sh/christian/ozone/api/generator/LexiconProcessingEnvironment.kt +++ b/generator/src/main/kotlin/sh/christian/ozone/api/generator/LexiconProcessingEnvironment.kt @@ -10,6 +10,7 @@ import java.io.File class LexiconProcessingEnvironment( allLexiconSchemaJsons: List, + val defaults: DefaultsConfiguration, val outputDirectory: File, ) : Iterable { private val schemasById: Map diff --git a/generator/src/main/kotlin/sh/christian/ozone/api/generator/TypeNames.kt b/generator/src/main/kotlin/sh/christian/ozone/api/generator/TypeNames.kt index 0b95cfc..c592ae3 100644 --- a/generator/src/main/kotlin/sh/christian/ozone/api/generator/TypeNames.kt +++ b/generator/src/main/kotlin/sh/christian/ozone/api/generator/TypeNames.kt @@ -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") diff --git a/generator/src/main/kotlin/sh/christian/ozone/api/generator/builder/LexiconDataClassesGenerator.kt b/generator/src/main/kotlin/sh/christian/ozone/api/generator/builder/LexiconDataClassesGenerator.kt index b5899e8..1ffbd7c 100644 --- a/generator/src/main/kotlin/sh/christian/ozone/api/generator/builder/LexiconDataClassesGenerator.kt +++ b/generator/src/main/kotlin/sh/christian/ozone/api/generator/builder/LexiconDataClassesGenerator.kt @@ -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 diff --git a/generator/src/main/kotlin/sh/christian/ozone/api/generator/builder/util.kt b/generator/src/main/kotlin/sh/christian/ozone/api/generator/builder/util.kt index 87c732d..9f085e4 100644 --- a/generator/src/main/kotlin/sh/christian/ozone/api/generator/builder/util.kt +++ b/generator/src/main/kotlin/sh/christian/ozone/api/generator/builder/util.kt @@ -145,45 +145,53 @@ fun createDataClass( fun createValueClass( className: ClassName, - serialName: String, + serialName: String?, innerType: TypeName, additionalConfiguration: TypeSpec.Builder.() -> Unit = {}, ): List { 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( @@ -202,7 +210,7 @@ fun createValueClass( .apply(additionalConfiguration) .build() - return listOf(serializerTypeSpec, valueClassTypeSpec) + return listOfNotNull(serializerTypeSpec, valueClassTypeSpec) } fun createObjectClass( diff --git a/generator/src/main/kotlin/sh/christian/ozone/api/generator/memberNames.kt b/generator/src/main/kotlin/sh/christian/ozone/api/generator/memberNames.kt index dbaf70b..4dde0b2 100644 --- a/generator/src/main/kotlin/sh/christian/ozone/api/generator/memberNames.kt +++ b/generator/src/main/kotlin/sh/christian/ozone/api/generator/memberNames.kt @@ -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") diff --git a/generator/src/main/kotlin/sh/christian/ozone/api/gradle/LexiconGeneratorExtension.kt b/generator/src/main/kotlin/sh/christian/ozone/api/gradle/LexiconGeneratorExtension.kt index abbd029..a09e8fb 100644 --- a/generator/src/main/kotlin/sh/christian/ozone/api/gradle/LexiconGeneratorExtension.kt +++ b/generator/src/main/kotlin/sh/christian/ozone/api/gradle/LexiconGeneratorExtension.kt @@ -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 @@ -21,6 +22,11 @@ abstract class LexiconGeneratorExtension internal val apiConfigurations: ListProperty = objects.listProperty().convention(emptyList()) + val namespace: Property = + objects.property().convention("sh.christian.ozone") + + internal val defaults = GeneratorDefaults(objects) + val outputDirectory: DirectoryProperty = objects.directoryProperty().convention( projectLayout.buildDirectory @@ -28,6 +34,10 @@ abstract class LexiconGeneratorExtension .map { it.dir("lexicons") } ) + fun defaults(configure: GeneratorDefaults.() -> Unit) { + defaults.configure() + } + fun generateApi( name: String, configure: ApiGeneratorExtension.() -> Unit = {}, @@ -35,10 +45,23 @@ abstract class LexiconGeneratorExtension apiConfigurations.add( ApiGeneratorExtension(name, objects) .apply(configure) - .apiConfiguration + .buildApiConfiguration(namespace.readFinalizedValue()) ) } + class GeneratorDefaults internal constructor( + objects: ObjectFactory, + ) { + val generateUnknownsForSealedTypes: Property = + objects.property().convention(false) + + internal fun buildDefaultsConfiguration(): DefaultsConfiguration { + return DefaultsConfiguration( + generateUnknownsForSealedTypes = generateUnknownsForSealedTypes.readFinalizedValue(), + ) + } + } + class ApiGeneratorExtension internal constructor( internal val name: String, objects: ObjectFactory, @@ -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(), diff --git a/generator/src/main/kotlin/sh/christian/ozone/api/gradle/LexiconGeneratorPlugin.kt b/generator/src/main/kotlin/sh/christian/ozone/api/gradle/LexiconGeneratorPlugin.kt index 2040eb8..e645f49 100644 --- a/generator/src/main/kotlin/sh/christian/ozone/api/gradle/LexiconGeneratorPlugin.kt +++ b/generator/src/main/kotlin/sh/christian/ozone/api/gradle/LexiconGeneratorPlugin.kt @@ -26,6 +26,8 @@ private fun Project.applyPlugin() { val generateLexicons = tasks.register("generateLexicons") { schemasClasspath.from(configuration) + namespace.set(extension.namespace) + defaults.set(extension.defaults.buildDefaultsConfiguration()) apiConfigurations.set(extension.apiConfigurations) outputDirectory.set(extension.outputDirectory) } diff --git a/generator/src/main/kotlin/sh/christian/ozone/api/gradle/LexiconGeneratorTask.kt b/generator/src/main/kotlin/sh/christian/ozone/api/gradle/LexiconGeneratorTask.kt index 481fb36..46112be 100644 --- a/generator/src/main/kotlin/sh/christian/ozone/api/gradle/LexiconGeneratorTask.kt +++ b/generator/src/main/kotlin/sh/christian/ozone/api/gradle/LexiconGeneratorTask.kt @@ -4,6 +4,7 @@ import org.gradle.api.DefaultTask import org.gradle.api.file.ConfigurableFileCollection import org.gradle.api.file.DirectoryProperty import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property import org.gradle.api.tasks.CacheableTask import org.gradle.api.tasks.Input import org.gradle.api.tasks.InputFiles @@ -13,6 +14,7 @@ import org.gradle.api.tasks.PathSensitivity.RELATIVE import org.gradle.api.tasks.SkipWhenEmpty import org.gradle.api.tasks.TaskAction import sh.christian.ozone.api.generator.ApiConfiguration +import sh.christian.ozone.api.generator.DefaultsConfiguration import sh.christian.ozone.api.generator.LexiconApiGenerator import sh.christian.ozone.api.generator.LexiconClassFileCreator import sh.christian.ozone.api.generator.LexiconProcessingEnvironment @@ -31,6 +33,12 @@ abstract class LexiconGeneratorTask : DefaultTask() { @get:PathSensitive(RELATIVE) abstract val schemasClasspath: ConfigurableFileCollection + @get:Input + abstract val namespace: Property + + @get:Input + abstract val defaults: Property + @get:Input abstract val apiConfigurations: ListProperty @@ -48,6 +56,7 @@ abstract class LexiconGeneratorTask : DefaultTask() { allLexiconSchemaJsons = schemasClasspath.flatMap { inputFile -> inputFile.toPath().findJsonFiles() }, + defaults = defaults.get(), outputDirectory = outputDir, ) @@ -65,6 +74,7 @@ abstract class LexiconGeneratorTask : DefaultTask() { } lexiconClassFileCreator.generateSealedRelationshipMapping() + lexiconClassFileCreator.generateSerializerModule(namespace.get()) lexiconApiGenerator.generateApis() } diff --git a/jetstream/build.gradle.kts b/jetstream/build.gradle.kts index 7021640..7fae6d9 100644 --- a/jetstream/build.gradle.kts +++ b/jetstream/build.gradle.kts @@ -24,7 +24,7 @@ kotlin { sourceSets { val commonMain by getting { dependencies { - implementation(libs.bluesky) + api(libs.bluesky) } } val jvmMain by getting {