Coverage Summary for Class: ProtoAnyRegistry (com.ghost.protobuf.wkt)
| Class |
Class, %
|
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| ProtoAnyRegistry |
100%
(1/1)
|
81.8%
(9/11)
|
64.3%
(9/14)
|
84%
(21/25)
|
80.7%
(142/176)
|
@file:OptIn(InternalGhostApi::class)
package com.ghost.protobuf.wkt
import com.ghost.protobuf.wkt.ProtoAnyRegistry.pack
import com.ghost.protobuf.wkt.ProtoAnyRegistry.register
import com.ghost.protobuf.wkt.ProtoAnyRegistry.unpack
import com.ghost.protobuf.wkt.ProtoAnyRegistry.unpackDynamic
import com.ghost.serialization.Ghost
import com.ghost.serialization.InternalGhostApi
import com.ghost.serialization.contract.GhostSerializer
import com.ghost.serialization.parser.GhostProtoJsonFlatReader
import kotlin.reflect.KClass
/**
* Maps `typeUrl` strings to the Kotlin types packed inside a [ProtoAny], so `pack`/`unpack`
* work without the caller manually juggling bytes.
*
* Registration is independent of [Ghost]'s own serializer registry — the type still needs a
* `GhostSerializer` registered there too (e.g. via `@GhostProtoSerialization` + KSP, or a manual
* [com.ghost.serialization.contract.GhostRegistry]). This registry only remembers which
* `typeUrl` string corresponds to which [KClass].
*
* ```kotlin
* ProtoAnyRegistry.register<DeviceRebooted>("type.googleapis.com/myapp.DeviceRebooted")
*
* val any: ProtoAny = ProtoAnyRegistry.pack(DeviceRebooted(deviceId = 1))
* val event: DeviceRebooted = ProtoAnyRegistry.unpack(any)
* val dynamic: Any? = ProtoAnyRegistry.unpackDynamic(any) // resolved purely from any.typeUrl
* ```
*/
object ProtoAnyRegistry {
private val typeUrlByClass = mutableMapOf<KClass<*>, String>()
private val classByTypeUrl = mutableMapOf<String, KClass<*>>()
/** Registers the `typeUrl` a [ProtoAny] should carry for messages of type [kClass]. */
fun register(typeUrl: String, kClass: KClass<*>) {
typeUrlByClass[kClass] = typeUrl
classByTypeUrl[typeUrl] = kClass
}
/** Reified convenience for [register]. */
inline fun <reified T : Any> register(typeUrl: String) {
register(typeUrl, T::class)
}
/** The `typeUrl` registered for [kClass], or `null` if none was registered. */
fun typeUrlFor(kClass: KClass<*>): String? = typeUrlByClass[kClass]
/** The [KClass] registered for [typeUrl], or `null` if none was registered. */
fun classFor(typeUrl: String): KClass<*>? = classByTypeUrl[typeUrl]
/**
* Serializes [message] and wraps it in a [ProtoAny] using the `typeUrl` registered for
* [kClass] via [register].
*
* @throws IllegalArgumentException if no `typeUrl` was registered for [kClass], or if
* [kClass] has no [GhostSerializer] registered with [Ghost] (e.g. missing
* `@GhostProtoSerialization`/`@GhostSerialization`).
*/
@Suppress("UNCHECKED_CAST")
fun <T : Any> pack(message: T, kClass: KClass<T>): ProtoAny {
val typeUrl = typeUrlByClass[kClass]
?: Ghost.throwError(
"No typeUrl registered for ${kClass.simpleName}. " +
"Call ProtoAnyRegistry.register<${kClass.simpleName}>(typeUrl) first."
)
val serializer = Ghost.getSerializer(kClass) as? GhostSerializer<T>
?: Ghost.throwError("${Ghost.NOT_FOUND} ${kClass.simpleName}. ${Ghost.MISSING_ANN}")
val bytes = Ghost.encodeToBytes(serializer, message)
return ProtoAny(typeUrl, bytes)
}
/** Reified convenience for [pack]. */
inline fun <reified T : Any> pack(message: T): ProtoAny = pack(message, T::class)
/**
* Decodes the payload captured in [any] as [kClass], using the [GhostSerializer] registered
* with [Ghost] for that type. Does not check [any]'s `typeUrl` against [kClass] — use
* [unpackDynamic] when the target type is only known from the wire.
*
* @throws IllegalArgumentException if [kClass] has no [GhostSerializer] registered with [Ghost].
*/
@Suppress("UNCHECKED_CAST")
fun <T : Any> unpack(any: ProtoAny, kClass: KClass<T>): T {
val serializer = Ghost.getSerializer(kClass) as? GhostSerializer<T>
?: Ghost.throwError("${Ghost.NOT_FOUND} ${kClass.simpleName}. ${Ghost.MISSING_ANN}")
return serializer.deserialize(GhostProtoJsonFlatReader(any.value))
}
/** Reified convenience for [unpack]. */
inline fun <reified T : Any> unpack(any: ProtoAny): T = unpack(any, T::class)
/**
* Resolves [any]'s Kotlin type purely from its `typeUrl` (via [register]) and decodes the
* payload, without the caller needing to know the target type at compile time.
*
* @return The decoded message, or `null` if no [KClass] was registered for `any.typeUrl`.
*/
fun unpackDynamic(any: ProtoAny): Any? {
val kClass = classByTypeUrl[any.typeUrl] ?: return null
val serializer = Ghost.getSerializer(kClass) ?: return null
return serializer.deserialize(GhostProtoJsonFlatReader(any.value))
}
/** Test-only: clears all registered typeUrl/KClass mappings. */
@InternalGhostApi
fun resetForTest() {
typeUrlByClass.clear()
classByTypeUrl.clear()
}
}