Skip to content

Commit

Permalink
Support subscriptions on all target platforms.
Browse files Browse the repository at this point in the history
The subscription endpoints moved from bsky.social to bsky.network, so we
also have to re-map those accordingly.
  • Loading branch information
christiandeange committed Apr 26, 2024
1 parent e25aafd commit bebfcbd
Show file tree
Hide file tree
Showing 21 changed files with 177 additions and 70 deletions.
2 changes: 1 addition & 1 deletion api-gen-runtime-internal/api/api-gen-runtime-internal.api
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public final class sh/christian/ozone/api/xrpc/XrpcSubscriptionParseException :

public final class sh/christian/ozone/api/xrpc/XrpcSubscriptionResponse {
public fun <init> ([B)V
public final fun body (Lkotlin/reflect/KClass;)Ljava/lang/Object;
public final fun body (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object;
public final fun component1 ()[B
public final fun copy ([B)Lsh/christian/ozone/api/xrpc/XrpcSubscriptionResponse;
public static synthetic fun copy$default (Lsh/christian/ozone/api/xrpc/XrpcSubscriptionResponse;[BILjava/lang/Object;)Lsh/christian/ozone/api/xrpc/XrpcSubscriptionResponse;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package sh.christian.ozone.api.xrpc

import kotlinx.serialization.KSerializer
import kotlin.reflect.KClass

typealias SubscriptionSerializerProvider<T> = (KClass<T>, String) -> KSerializer<T>?
Original file line number Diff line number Diff line change
@@ -1,11 +1,29 @@
package sh.christian.ozone.api.xrpc

import io.ktor.client.HttpClient
import io.ktor.client.plugins.api.ClientPlugin
import io.ktor.client.plugins.api.SendingRequest
import io.ktor.client.plugins.api.createClientPlugin
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.websocket.WebSockets
import io.ktor.http.URLProtocol
import io.ktor.http.Url
import io.ktor.http.takeFrom
import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.json.Json

internal val BSKY_SOCIAL = Url("https://bsky.social")
internal val BSKY_NETWORK = Url("https://bsky.network")

internal object WebsocketRedirectPlugin : ClientPlugin<Unit>
by createClientPlugin("WebsocketRedirect", {
on(SendingRequest) { request, _ ->
if (request.url.protocol == URLProtocol.WS || request.url.protocol == URLProtocol.WSS) {
request.url.host = BSKY_NETWORK.host
}
}
})

expect val defaultHttpClient: HttpClient

fun HttpClient.withXrpcConfiguration(): HttpClient = config {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,15 +103,23 @@ suspend inline fun <reified T : Any> HttpResponse.toAtpResponse(): AtpResponse<T
}
}

inline fun <reified T : Any> Flow<XrpcSubscriptionResponse>.toAtpModel(): Flow<T> =
toAtpResult<T>().map { it.getOrThrow() }
inline fun <reified T : Any> Flow<XrpcSubscriptionResponse>.toAtpModel(
noinline subscriptionSerializerProvider: SubscriptionSerializerProvider<T>,
): Flow<T> {
return toAtpResult<T>(subscriptionSerializerProvider).map { it.getOrThrow() }
}

@OptIn(ExperimentalSerializationApi::class)
inline fun <reified T : Any> Flow<XrpcSubscriptionResponse>.toAtpResult(): Flow<Result<T>> =
map { response -> runCatching { response.body<T>() } }
inline fun <reified T : Any> Flow<XrpcSubscriptionResponse>.toAtpResult(
noinline subscriptionSerializerProvider: SubscriptionSerializerProvider<T>,
): Flow<Result<T>> {
return map { response -> runCatching { response.body(subscriptionSerializerProvider) } }
}

inline fun <reified T : Any> Flow<XrpcSubscriptionResponse>.toAtpResponse(): Flow<AtpResponse<T>> =
toAtpResult<T>().map {
inline fun <reified T : Any> Flow<XrpcSubscriptionResponse>.toAtpResponse(
noinline subscriptionSerializerProvider: SubscriptionSerializerProvider<T>,
): Flow<AtpResponse<T>> {
return toAtpResult<T>(subscriptionSerializerProvider).map {
it.fold(
onSuccess = { body ->
AtpResponse.Success(
Expand All @@ -129,3 +137,4 @@ inline fun <reified T : Any> Flow<XrpcSubscriptionResponse>.toAtpResponse(): Flo
}
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ package sh.christian.ozone.api.xrpc

import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.InternalSerializationApi
import kotlinx.serialization.cbor.Cbor
import kotlinx.serialization.serializer
import sh.christian.ozone.api.cbor.ByteArrayInput
import sh.christian.ozone.api.cbor.CborDecoder
import sh.christian.ozone.api.cbor.CborReader
Expand All @@ -14,17 +15,23 @@ data class XrpcSubscriptionResponse(
val bytes: ByteArray,
) {
@ExperimentalSerializationApi
inline fun <reified T : Any> body(): T = body(T::class)
inline fun <reified T : Any> body(
noinline subscriptionSerializerProvider: SubscriptionSerializerProvider<T>,
): T = body(T::class, subscriptionSerializerProvider)

@OptIn(InternalSerializationApi::class)
@ExperimentalSerializationApi
fun <T : Any> body(kClass: KClass<T>): T {
fun <T : Any> body(
kClass: KClass<T>,
subscriptionSerializerProvider: SubscriptionSerializerProvider<T>,
): T {
val frame = decodeFromByteArray(XrpcSubscriptionFrame.serializer(), bytes)

val payloadPosition = bytes.drop(1).indexOfFirst { it.toInt().isCborMapStart() } + 1
val payloadBytes = bytes.drop(payloadPosition).toByteArray()

if (frame.op == 1 && frame.t != null) {
val serializer = getSerializer(kClass, frame)
val serializer = subscriptionSerializerProvider(kClass, frame.t) ?: kClass.serializer()
return decodeFromByteArray(serializer, payloadBytes)
} else {
val maybeError = runCatching { decodeFromByteArray(AtpErrorDescription.serializer(), payloadBytes) }.getOrNull()
Expand Down Expand Up @@ -61,8 +68,3 @@ data class XrpcSubscriptionResponse(
}

internal fun Int.isCborMapStart(): Boolean = (this and 0b11100000) == 0b10100000

internal expect fun <T : Any> getSerializer(
kClass: KClass<T>,
frame: XrpcSubscriptionFrame,
): KSerializer<out T>
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import io.ktor.http.takeFrom

actual val defaultHttpClient: HttpClient = HttpClient(Darwin) {
install(DefaultRequest) {
url.takeFrom("https://bsky.social")
url.takeFrom(BSKY_SOCIAL)
}

install(WebsocketRedirectPlugin)
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import io.ktor.http.takeFrom

actual val defaultHttpClient: HttpClient = HttpClient(Js) {
install(DefaultRequest) {
url.takeFrom("https://bsky.social")
url.takeFrom(BSKY_SOCIAL)
}
}

install(WebsocketRedirectPlugin)
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import io.ktor.http.takeFrom

actual val defaultHttpClient: HttpClient = HttpClient(CIO) {
install(DefaultRequest) {
url.takeFrom("https://bsky.social")
url.takeFrom(BSKY_SOCIAL)
}
}

install(WebsocketRedirectPlugin)
}

This file was deleted.

1 change: 1 addition & 0 deletions app/desktop/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ kotlin {
dependencies {
implementation(project(":app:common"))
implementation(compose.desktop.currentOs)
implementation(kotlin("reflect"))
}
}
}
Expand Down
4 changes: 4 additions & 0 deletions bluesky/api/bluesky.api
Original file line number Diff line number Diff line change
Expand Up @@ -9259,6 +9259,10 @@ public final class sh/christian/ozone/XrpcBlueskyApi : sh/christian/ozone/Bluesk
public fun uploadBlob ([BLkotlin/coroutines/Continuation;)Ljava/lang/Object;
}

public final class sh/christian/ozone/api/xrpc/FindSubscriptionSerializerKt {
public static final fun findSubscriptionSerializer (Lkotlin/reflect/KClass;Ljava/lang/String;)Lkotlinx/serialization/KSerializer;
}

public final class tools/ozone/communication/CreateTemplateRequest {
public static final field Companion Ltools/ozone/communication/CreateTemplateRequest$Companion;
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,11 @@ class LexiconApiGenerator(
}

unindent()
add(").%M()", transformingMethodName)
when (apiCall) {
is Query -> add(").%M()", transformingMethodName)
is Procedure -> add(").%M()", transformingMethodName)
is Subscription -> add(").%M(::%M)", transformingMethodName, findSubscriptionSerializer)
}

if (!configuration.suspending) {
add("\n")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,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.ParameterizedTypeName.Companion.parameterizedBy
import com.squareup.kotlinpoet.TypeVariableName
import com.squareup.kotlinpoet.withIndent
import org.gradle.configurationcache.extensions.capitalized
import sh.christian.ozone.api.generator.builder.GeneratorContext
import sh.christian.ozone.api.generator.builder.LexiconDataClassesGenerator
import sh.christian.ozone.api.generator.builder.SealedRelationship
import sh.christian.ozone.api.generator.builder.TypesGenerator
import sh.christian.ozone.api.generator.builder.XrpcBodyGenerator
import sh.christian.ozone.api.generator.builder.XrpcQueryParamsGenerator
Expand All @@ -21,6 +28,8 @@ class LexiconClassFileCreator(
XrpcBodyGenerator(environment),
)

private val sealedRelationships = mutableListOf<SealedRelationship>()

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

Expand All @@ -37,6 +46,8 @@ class LexiconClassFileCreator(
}
context.types().forEach { addType(it) }
context.typeAliases().forEach { addTypeAlias(it) }

sealedRelationships += context.sealedRelationships()
}
.addAnnotation(
AnnotationSpec.builder(TypeNames.Suppress)
Expand All @@ -63,4 +74,57 @@ class LexiconClassFileCreator(
enumFile.writeTo(environment.outputDirectory)
}
}

fun generateSealedRelationshipMapping() {
val relationships = sealedRelationships.groupBy { it.sealedInterface }

FileSpec.builder(findSubscriptionSerializer.packageName, findSubscriptionSerializer.simpleName)
.addFunction(
FunSpec.builder(findSubscriptionSerializer)
.addAnnotation(
AnnotationSpec.builder(TypeNames.Suppress)
.addMember("%S", "UNCHECKED_CAST")
.build()
)
.addTypeVariable(TypeVariableName("T", Any::class))
.addParameter("parentType", TypeNames.KClass.parameterizedBy(TypeVariableName("T")))
.addParameter("serialName", String::class)
.returns(
TypeNames.KSerializer
.parameterizedBy(TypeVariableName("T", variance = KModifier.OUT))
.copy(nullable = true)
)
.addCode(
CodeBlock.builder()
.add("return when(parentType) {\n")
.withIndent {
relationships.forEach { (parent, children) ->
add("%T::class -> when {\n", parent)
withIndent {
children.forEach { child ->
addStatement(
"%S.endsWith(serialName) ->\n%T.serializer()",
child.childClassSerialName,
child.childClass,
)
}
addStatement("else -> null")
}
addStatement("}")
}
addStatement("else -> null")
}
.add(
"} as %T",
TypeNames.KSerializer
.parameterizedBy(TypeVariableName("T", variance = KModifier.OUT))
.copy(nullable = true),
)
.build()
)
.build()
)
.build()
.writeTo(environment.outputDirectory)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ object TypeNames {
val Handle by classOfPackage("sh.christian.ozone.api")
val HttpClient by classOfPackage("io.ktor.client")
val JsonContent by classOfPackage("sh.christian.ozone.api.model")
val KClass by classOfPackage("kotlin.reflect")
val KSerializer by classOfPackage("kotlinx.serialization")
val Language by classOfPackage("sh.christian.ozone.api")
val Nsid by classOfPackage("sh.christian.ozone.api")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ private constructor(
private val enums = mutableMapOf<ClassName, MutableSet<String>>()
private val types = mutableMapOf<ClassName, TypeSpec>()
private val typeAliases = mutableMapOf<ClassName, TypeAliasSpec>()
private val sealedRelationships = mutableListOf<SealedRelationship>()

fun addEnum(
className: ClassName,
Expand All @@ -44,11 +45,25 @@ private constructor(
typeAliases += typeAliasLocation to typeAliasSpec
}

fun addSealedRelationship(
sealedInterface: ClassName,
childClass: ClassName,
childClassSerialName: String,
) {
sealedRelationships += SealedRelationship(
sealedInterface = sealedInterface,
childClass = childClass,
childClassSerialName = childClassSerialName,
)
}

fun enums(): Map<ClassName, Set<String>> {
return enums.mapValues { it.value.toSet() }
}

fun types(): Set<TypeSpec> = types.values.toSet()

fun typeAliases(): Set<TypeAliasSpec> = typeAliases.values.toSet()

fun sealedRelationships(): List<SealedRelationship> = sealedRelationships.toList()
}
Loading

0 comments on commit bebfcbd

Please sign in to comment.