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

implement a converter to dynamic message #212

Conversation

andrewparmet
Copy link
Collaborator

No description provided.

inline fun <reified T : KtMessage> Any.isA() =
typeUrl.substringAfterLast('/') ==
(
T::class.java.getAnnotation(KtGeneratedMessage::class.java)?.fullTypeName
?: T::class.java.getAnnotation(com.toasttab.protokt.rt.KtGeneratedMessage::class.java)?.fullTypeName
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this was removed a few PRs ago so it's dead code

Comment on lines 50 to 74
fun KtMessage.toDynamicMessage(context: RuntimeContext): DynamicMessage =
context.convertValue(this) as DynamicMessage

fun KtMessage.hasField(field: FieldDescriptor): Boolean {
val value = getField(field)

return if (field.hasPresence()) {
value != null
} else {
value != defaultValue(field)
}
}

private fun defaultValue(field: FieldDescriptor) =
when (field.type) {
Type.UINT64, Type.FIXED64 -> 0uL
Type.UINT32, Type.FIXED32 -> 0u
Type.BYTES -> Bytes.empty()
else -> field.defaultValue
}

fun KtMessage.getField(field: FieldDescriptor) =
ProtoktReflect.getField(this, field)

class RuntimeContext internal constructor(
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New public APIs. Open to revision before 1.0.

Comment on lines -19 to -22
import com.squareup.kotlinpoet.CodeBlock
import com.squareup.kotlinpoet.MemberName
import com.squareup.kotlinpoet.MemberName.Companion.member
import com.squareup.kotlinpoet.asTypeName
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removing kotlinpoet deps from code now used in reflection at runtime

@andrewparmet andrewparmet marked this pull request as ready for review February 7, 2024 17:02
@andrewparmet andrewparmet deleted the implement-converter-to-dynamic-message branch February 7, 2024 19:02
@andrewparmet andrewparmet restored the implement-converter-to-dynamic-message branch February 13, 2024 02:17
@andrewparmet andrewparmet reopened this Feb 13, 2024
import protokt.v1.codegen.util.KotlinPlugin
import protokt.v1.codegen.util.Message
import protokt.v1.codegen.util.Oneof
import protokt.v1.codegen.util.StandardField
import protokt.v1.codegen.util.Tag
import protokt.v1.reflect.FieldType
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of these types that are used for codegen but are also going to be used in dynamic conversion are moved to shared source and stripped of any bad runtime deps, e.g. KotlinPoet. They're staying internal so the public API does not expose them.

@@ -30,3 +36,30 @@ class ProtoktCodegenTest : AbstractProtoktCodegenTest() {
}
}
}

object UuidBytesConverter : AbstractConverter<Bytes, UUID>(), OptimizedSizeOfConverter<Bytes, UUID> {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Allow testing of wrapper types with a debugger.

srcDir(rootProject.file("shared-src/reflect"))
}
proto {
srcDir("../extensions/protokt-extensions-lite/src/extensions-proto")
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generate the protokt extensions with Java for this module to be able to load its descriptor to get our custom options in the reflect runtime.

Comment on lines 53 to 71
fun KtMessage.hasField(field: FieldDescriptor): Boolean {
val value = getField(field)

return if (field.hasPresence()) {
value != null
} else {
value != defaultValue(field)
}
}

private fun defaultValue(field: FieldDescriptor) =
when (field.type) {
Type.UINT64, Type.FIXED64 -> 0uL
Type.UINT32, Type.FIXED32 -> 0u
Type.BYTES -> Bytes.empty()
else -> field.defaultValue
}

fun KtMessage.getField(field: FieldDescriptor) =
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hasField and getField are the two APIs needed to run the Buf validator.

}.run {
val table = HashBasedTable.create<String, String, MutableList<Converter<*, *>>>()
forEach { table.getOrPut(it.wrapped.qualifiedName!!, it.wrapper.qualifiedName!!) { mutableListOf() }.add(it) }
ImmutableTable.builder<String, String, List<Converter<*, *>>>().putAll(table).build()
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing Guava from the reflect runtime's dependencies.


package protokt.v1.reflect

internal fun inferClassName(className: String, pkg: String): Pair<String, List<String>> {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are more or less copied from KotlinPoet since we need them for reflection.

data object SInt64 : Scalar(Long::class)
data object UInt32 : Scalar(UInt::class)
data object UInt64 : Scalar(ULong::class)
object Bool : Scalar(Boolean::class)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need compatibility with Kotlin 1.8

sourceSets {
test {
java {
srcDir(rootProject.file("shared-src/lite-util"))
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

forgotten edge case - good to have the lite tests here.

Copy link
Member

@ogolberg ogolberg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comments inline.

My other question is - can we just codegenerate a converter from KtMesage to DynamicMessage for each KtMessage without using any reflection?

}

private fun getDescriptors() =
ClassGraph()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we generate an annotation on the message class that points to the descriptor?

I feel really uncomfortable scanning the whole classpath for this

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps. We can't point to it directly, since it won't exist in lite code, but maybe there's a way to make reflection direct instead of scanned.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay - the reason this is implemented reflectively is for convenience. If we had an annotation on a message in your hypothetical case, you still have to get those annotations, either by scanning or by explicit reference. If you have them explicitly then you may as well reference the descriptors themselves instead to build your own RuntimeContext.

If we don't want to encourage this kind of scan, which should only be done once, e.g. at application initialization to build validators, then we can remove this code and require users to write this themselves.


internal val descriptorsByTypeName = descriptors.associateBy { it.fullName }

fun convertValue(value: Any?) =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not obvious what this public API method does

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can make this not take a nullable argument for starters.

Would naming it better be a good idea? I think it has to be able to take any type since it's called at runtime with whatever field value the descriptor extracts from a protokt message.

A more descriptive name could be convertProtoktToProtobufJava.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason this has to be public is to enable sub-message conversion in protovalidate. If it's acceptable to convert the whole message every time, even if some portion of the message may never be validated, then this can be internal.

@andrewparmet
Copy link
Collaborator Author

can we just codegenerate a converter from KtMesage to DynamicMessage for each KtMessage without using any reflection?

We could, but that feels a bit messy. I haven't benchmarked this yet but I think it's a big enough step forward in terms of permitting message validation that it's worth having.

This doesn't prevent us from doing that in the future, of course.

@andrewparmet
Copy link
Collaborator Author

Code generation of a converter gets tricky - is there a way to do it without exposing a dependency on protobuf-java?

@andrewparmet andrewparmet merged commit aca2830 into open-toast:main May 8, 2024
13 checks passed
@andrewparmet andrewparmet deleted the implement-converter-to-dynamic-message branch May 8, 2024 18:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants